In this article, we will create an attractive Azure Maps autocomplete widget that you can use to visually generate Azure Maps in your own web application using jQuery and Kendo Core, both of which are free and open-source. This is the same widget I programmed for Galaxie Blog, which generated the map you see above.



Working Azure Maps AutoSuggest Example


The following working example uses the same code shown here.


 


Load jQuery and Kendo UI


In this example, I am implementing the autosuggest widget provided by Kendo UI Core and jQuery. In this example, I am using a CDN for jQuery and incorporating a local version of Kendo Core. See Incorporate Kendo UI into a ColdFusion Application for more information.


<!-- Load jQuery -->
<script rel="preconnect"
	src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"
	crossorigin="anonymous">
<!-- Load Kendo script -->
<script src="/blog/common/libs/kendoCore/js/kendo.ui.core.min.js" type="text/javascript">
<!-- Kendo CSS -->
<link rel="stylesheet" href="/blog/common/libs/kendo/styles/kendo.common.min.css" />
<link rel="stylesheet" href="/blog/common/libs/kendo/styles/kendo.default.min.css" />

CSS


The following CSS sets the visual properties of the container where Azure Maps is rendered. In this example, I have two containers: the sidebar, which holds the country dropdown and search form, and is 350 pixels wide, and a container that takes up the remaining part of the screen to the right, where the map is rendered. 


.sidePanel {
	width: 350px;
	height: 100%;
	float: left;
	margin-right: 10px;
}

#myMap {
	position: relative;
	width: calc(100% - 360px);
	min-width:290px;
	height: 600px;
	float: left;
}

JavaScript


Place the Kendo AutoSuggest Widget inside the document-ready Block


As with most Kendo UI Widgets, execute the code after the Document Object Model (DOM) is ready to ensure that the autosuggest Kendo UI widget interacts correctly with elements on the webpage. 


$(document).ready(function(){

Add the Azure Map Control 


Since we are loading a static map preview based on the search results, we need to load the required CSS before loading the JavaScript controller. The script can be loaded anywhere before the page's closing body. 


<!-- Add references to the Azure Maps Map control JavaScript and CSS files. -->
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.css" type="text/css">
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.js">

Preset the Azure Maps Fuzzy Service URL


This URL can be used in the AJAX read declaration; however, I prefer to set it as a JavaScript variable, making it easier to change the service endpoints when Microsoft updates them.


// Fuzzy search includes POI and addresses
var fuzzyGeoServiceUrl = "https://atlas.microsoft.com/search/fuzzy/json?typeahead=true&api-version=1.0&language=en-US&lon=0&lat=0&view=Auto"; 

Create the Kendo UI AutoComplete Widget


Initialize the Country Kendo UI Dropdown List


The following code renders a Kendo Dropdown list from static HTML. This dropdown is used to limit the Azure Maps search by the selected country code. Using the country to restrict the search results is not required; however, it is recommended practice when using Azure Maps' fuzzy search, which provides a better user experience and reduces your free tile usage allotment and traffic between the client and Azure Cloud servers.


// create DropDownList from select HTML element
$("#countrySelector").kendoDropDownList();

Create the Kendo UI Datasource


The Kendo Datasource uses AJAX to pass the selected country and location, along with the Azure Maps subscription key, to the Azure Maps fuzzy search endpoint. The subscription key is passed using ColdFusion in a cfoutput tag. Replace '<cfoutput>#azureMapsKey#</cfoutput>' with your own Azure Maps API key.

In the success callback, the JSON results returned by the endpoint are passed to my custom parseResults function, which flattens the complex JSON, as shown below. The schema uses the location and GeoCoordinates values returned from the endpoint to populate the autoSuggest widget.


var locationDs = new kendo.data.DataSource({
	transport: {
		read: function(options) {

			// Perform a custom the AJAX request to the Azure Maps API
			$.ajax({
				url: fuzzyGeoServiceUrl, // the URL of the API endpoint.
				type: "get",// Azure maps require the get method and posts will fail with a 505 eror
				data: {
					// Pass the key. The dash will cause an error if the arg is not enclosed in a string
					'subscription-key': <cfoutput>'#azureMapsKey#'</cfoutput>,
					 // Pass the value typed in to the form for the query parameter
					query: function(){
						return $("#location").data("kendoAutoComplete").value();
					},//..query
					// Pass the selected country
					countrySet: function(){
						return $("#countrySelector").data("kendoDropDownList").value();
					},//..countrySet

				},//..data
				dataType: "json", // Use json if the template is on the current server. If not, use jsonp for cross domain reads.
				success: function(result) {
					// If the request is successful, call the options.success callback
					options.success( parseResponse(result) );
				},
				error: function(error) {
					// If the request fails, call the options.error callback
					options.error(error);
				}
			});//ajax

		},//read
		schema: {
			model: {
				fields: {
					freeformAddress: {type: "string" },
					lat: {type: "string" },
					lon: {type: "string" },
					topLeftPointLon: {type: "string" },
					topLeftPointLat: {type: "string" },
					btmRightPointLon: {type: "string" },
					btmRightPointLat: {type: "string" }
				}//fields
			}//model
		}//schema
	},//transport
	cache: false,
	serverFiltering: true // without this argument, the autocomplete will not work and only fire the ajax request once
});

Declare the Kendo UI AutoComplete Widget


The AutoComplete uses the values stored within the flattened JSON and stores the selected index, address, and the GeoCoordinates to populate the AutoComplete widget. Once the user chooses an option, these values are saved as hidden form values and used to render the Azure Map using the getMap custom function.


$("#location").kendoAutoComplete({
	minLength: 3,
	dataSource: locationDs, 
	dataTextField: "label", // The widget is bound to the "label" 
	select: function(e) {
		// Store the selected index
		$("#selectedIndex").val(e.item.index());
		// Read the items in the datasource using the selected index
		var selectedLocation = locationDs.at( e.item.index() );
		// Save the values in a hidden form
		$("#selectedFreeformAddress").val(selectedLocation.freeformAddress);
		$("#selectedLat").val(selectedLocation.lat);
		$("#selectedLon").val(selectedLocation.lon);
		$("#selectedTopLeftPointLon").val(selectedLocation.topLeftPointLon);
		$("#selectedTopLeftPointLat").val(selectedLocation.topLeftPointLat);
		$("#selectedBtmRightPointLon").val(selectedLocation.btmRightPointLon);
		$("#selectedBtmRightPointLat").val(selectedLocation.btmRightPointLat);
		// Write the selected index to the console for debugging
		console.log(selectedLocation);

		// Render the map
		getMap();
	}
});

Handle Azure Maps' Complex JSON Using the Parse Response Function


The following two JavaScript functions are used to flatten the JSON returned by the Azure Maps API. Unfortunately, the JSON returned by the Azure Maps API is complex and has nested structures, and we must implement additional logic to flatten it for Kendo UI. The following script takes the JSON returned from the Azure Maps service via the datasource and flattens it for use by the Kendo AutoComplete widget.

I have covered this technique in a separate article on using Kendo with complex JSON. 


function parseResponse(obj){

	// https://stackoverflow.com/questions/15009448/creating-a-json-dynamically-with-each-input-value-using-jquery
	// Instantiate the json objects
	jsonObj = [];

	// Loop through the items in the object
	for (var i = 0; i < obj.results.length; i++) {
		if (obj.results[i]) {
			// Get the data from the object
			var results = obj.results[i];// Results is an array in the json returned from the server

			// The POI is only available if the type is POI 
			var poi = '';
			var label = results.address.freeformAddress;
			if (results.type === 'POI'){
				poi = results.poi.name;	
				// Now that we have the POI when it exists, set the label that we will use. We will use the POI Name if it exists, otherwise we will use the freeFormAddress
				label = poi;
			}

			// Create the struct. We need the latitute, longitude and the POI if it exists. 
			let jsonItems = {
				freeformAddress: results.address.freeformAddress,
				poi: poi,
				label: label,
				lat: results.position.lat,
				lon: results.position.lon,
				topLeftPointLon: results.viewport.topLeftPoint.lon,
				topLeftPointLat: results.viewport.topLeftPoint.lat,
				btmRightPointLon: results.viewport.btmRightPoint.lon, 
				btmRightPointLat: results.viewport.btmRightPoint.lat
			};
			// Push the items into the new json object
			jsonObj.push(jsonItems);
		}
	}//..for
	// Write the object out for testing
	console.log(jsonObj);
	// And return it...
	return jsonObj;
}

Close the Document Ready Block Before Creating the Azure GetMap Function

Close the document-ready block where the Kendo auto-suggest widget was placed. This is necessary because Azure Maps has its own map-ready event, and mixing the two may cause undesired behavior and break the code.


Rendering the Azure Map


Here, I retrieve the form values, declare the map, pass my Azure Maps subscription key, and render the Azure Map along with the map controls in the Azure Maps ready event. All of this logic is covered in detail in my previous article, "Creating Static Maps with Azure Maps."


function getMap() {

	// Get the necessary valus from the hidden form values
	var freeformAddress = $("#selectedFreeformAddress").val();
	var lat = $("#selectedLat").val();
	var lon = $("#selectedLon").val();
	// Camera positions
	var topLeftPointLat = $("#selectedTopLeftPointLat").val();
	var topLeftPointLon = $("#selectedTopLeftPointLon").val();
	var btmRightPointLat = $("#selectedBtmRightPointLat").val();
	var btmRightPointLon = $("#selectedBtmRightPointLon").val();

	// Initialize a map instance.
	map = new atlas.Map('myMap', {
		view: 'Auto',
		authOptions: {
			 authType: 'subscriptionKey',
			 subscriptionKey: '<cfoutput>#azureMapsKey#</cfoutput>'
		 }
	});

	// Wait until the map resources are ready.
	map.events.add('ready', function () {
		// Load the custom image icon into the map resources. This must be done immediately after the ready event
		map.imageSprite.add('map-marker', '<cfoutput>#application.defaultAzureMapsCursor#</cfoutput>').then(function () {
			// Create a data source to store the data in.
			datasource = new atlas.source.DataSource();
			// Add the datasource
			map.sources.add(datasource);
			// Add a layer for rendering point data.
			map.layers.add(new atlas.layer.SymbolLayer(datasource));
			// Remove any previous added data from the map.
			datasource.clear();
			// Create a point feature to mark the selected location.
			datasource.add(new atlas.data.Feature(new atlas.data.Point([lon,lat])));
			//datasource.add(new atlas.data.Feature(new atlas.data.Point([lon,lat]), ui.item));

			// Zoom the map into the selected location.
			map.setCamera({
				bounds: [
					topLeftPointLon, btmRightPointLat,
					btmRightPointLon, topLeftPointLat
				],
				padding: 0
			});//map.setCamera

			// Add the controls

			// Create a zoom control.
			map.controls.add(new atlas.control.ZoomControl({
				zoomDelta: parseFloat(1),
				style: "light"
		   }), {
			  position: 'top-right'
			}); 

			// Create the style control
			map.controls.add(new atlas.control.StyleControl({
			  mapStyles: ['road', 'grayscale_dark', 'night', 'road_shaded_relief', 'satellite', 'satellite_road_labels'],
			  layout: 'icons'
			}), {
			  position: 'top-right'
			});  
		});//map.imageSprite.add...
	})//map.events

}//getMap

HTML


The HTML calls the getMap function on the body's onload event. The sidePanel div divides the page into two sections, separating the search interface from the myMap div that contains the rendered map content. The search interface includes hidden form fields to store selected values and a country dropdown to filter Azure Maps results by the chosen country. Finally, we have the location form field, which lets the user search for a location. 


<!-- Load the map using the body -->
<body onload="getMap()">

	<div class="sidePanel">

		<table cellpadding="0" cellspacing="0" class="k-content" style="width:320px;">
			<!-- Hidden inputs to store user selections -->
			<input type="hidden" name="selectedFreeformAddress" id="selectedFreeformAddress" value="" /> 
			<!-- Latitute -->
			<input type="hidden" name="selectedLat" id="selectedLat" value="" />
			<!-- Longitude -->
			<input type="hidden" name="selectedLon" id="selectedLon" value="" />

			<!-- Camera Top Left Latitude -->
			<input type="hidden" name="selectedTopLeftPointLat" id="selectedTopLeftPointLat" value="" />
			<!-- Camera Top Left Longitude -->
			<input type="hidden" name="selectedTopLeftPointLon" id="selectedTopLeftPointLon" value="" />
			<!-- Camera Bottom Right Latitude -->
			<input type="hidden" name="selectedBtmRightPointLat" id="selectedBtmRightPointLat" value="" />
			<!-- Camera Bottom Right Longitude -->
			<input type="hidden" name="selectedBtmRightPointLon" id="selectedBtmRightPointLon" value="" />

			<tr style="height: 35px;">
				<td width="15%" align="right">
					<label>Country:</label>
				</td>
				<td width="*">
					<select id="countrySelector" style="width:66%">
						<option value="AF">Afghanistan</option>
						<option value="AX">Ã…land Islands</option>
						<option value="AL">Albania</option>
						<option value="DZ">Algeria</option>
						<option value="AS">American Samoa</option>
						<option value="AD">Andorra</option>
						<option value="AO">Angola</option>
						<option value="AI">Anguilla</option>
						<option value="AQ">Antarctica</option>
						<option value="AG">Antigua and Barbuda</option>
						<option value="AR">Argentina</option>
						<option value="AM">Armenia</option>
						<option value="AW">Aruba</option>
						<option value="AU">Australia</option>
						<option value="AT">Austria</option>
						<option value="AZ">Azerbaijan</option>
						<option value="BS">Bahamas</option>
						<option value="BH">Bahrain</option>
						<option value="BD">Bangladesh</option>
						<option value="BB">Barbados</option>
						<option value="BY">Belarus</option>
						<option value="BE">Belgium</option>
						<option value="BZ">Belize</option>
						<option value="BJ">Benin</option>
						<option value="BM">Bermuda</option>
						<option value="BT">Bhutan</option>
						<option value="BO">Bolivia (Plurinational State of)</option>
						<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
						<option value="BA">Bosnia and Herzegovina</option>
						<option value="BW">Botswana</option>
						<option value="BV">Bouvet Island</option>
						<option value="BR">Brazil</option>
						<option value="IO">British Indian Ocean Territory</option>
						<option value="BN">Brunei Darussalam</option>
						<option value="BG">Bulgaria</option>
						<option value="BF">Burkina Faso</option>
						<option value="BI">Burundi</option>
						<option value="CV">Cabo Verde</option>
						<option value="KH">Cambodia</option>
						<option value="CM">Cameroon</option>
						<option value="CA">Canada</option>
						<option value="KY">Cayman Islands</option>
						<option value="CF">Central African Republic</option>
						<option value="TD">Chad</option>
						<option value="CL">Chile</option>
						<option value="CN">China</option>
						<option value="CX">Christmas Island</option>
						<option value="CC">Cocos (Keeling) Islands</option>
						<option value="CO">Colombia</option>
						<option value="KM">Comoros</option>
						<option value="CG">Congo</option>
						<option value="CD">Congo, Democratic Republic of the</option>
						<option value="CK">Cook Islands</option>
						<option value="CR">Costa Rica</option>
						<option value="CI">Côte d'Ivoire</option>
						<option value="HR">Croatia</option>
						<option value="CU">Cuba</option>
						<option value="CW">Curaçao</option>
						<option value="CY">Cyprus</option>
						<option value="CZ">Czechia</option>
						<option value="DK">Denmark</option>
						<option value="DJ">Djibouti</option>
						<option value="DM">Dominica</option>
						<option value="DO">Dominican Republic</option>
						<option value="EC">Ecuador</option>
						<option value="EG">Egypt</option>
						<option value="SV">El Salvador</option>
						<option value="GQ">Equatorial Guinea</option>
						<option value="ER">Eritrea</option>
						<option value="EE">Estonia</option>
						<option value="SZ">Eswatini</option>
						<option value="ET">Ethiopia</option>
						<option value="FK">Falkland Islands (Malvinas)</option>
						<option value="FO">Faroe Islands</option>
						<option value="FJ">Fiji</option>
						<option value="FI">Finland</option>
						<option value="FR">France</option>
						<option value="GF">French Guiana</option>
						<option value="PF">French Polynesia</option>
						<option value="TF">French Southern Territories</option>
						<option value="GA">Gabon</option>
						<option value="GM">Gambia</option>
						<option value="GE">Georgia</option>
						<option value="DE">Germany</option>
						<option value="GH">Ghana</option>
						<option value="GI">Gibraltar</option>
						<option value="GR">Greece</option>
						<option value="GL">Greenland</option>
						<option value="GD">Grenada</option>
						<option value="GP">Guadeloupe</option>
						<option value="GU">Guam</option>
						<option value="GT">Guatemala</option>
						<option value="GG">Guernsey</option>
						<option value="GN">Guinea</option>
						<option value="GW">Guinea-Bissau</option>
						<option value="GY">Guyana</option>
						<option value="HT">Haiti</option>
						<option value="HM">Heard Island and McDonald Islands</option>
						<option value="VA">Holy See</option>
						<option value="HN">Honduras</option>
						<option value="HK">Hong Kong</option>
						<option value="HU">Hungary</option>
						<option value="IS">Iceland</option>
						<option value="IN">India</option>
						<option value="ID">Indonesia</option>
						<option value="IR">Iran (Islamic Republic of)</option>
						<option value="IQ">Iraq</option>
						<option value="IE">Ireland</option>
						<option value="IM">Isle of Man</option>
						<option value="IL">Israel</option>
						<option value="IT">Italy</option>
						<option value="JM">Jamaica</option>
						<option value="JP">Japan</option>
						<option value="JE">Jersey</option>
						<option value="JO">Jordan</option>
						<option value="KZ">Kazakhstan</option>
						<option value="KE">Kenya</option>
						<option value="KI">Kiribati</option>
						<option value="KP">Korea (Democratic People's Republic of)</option>
						<option value="KR">Korea, Republic of</option>
						<option value="KW">Kuwait</option>
						<option value="KG">Kyrgyzstan</option>
						<option value="LA">Lao People's Democratic Republic</option>
						<option value="LV">Latvia</option>
						<option value="LB">Lebanon</option>
						<option value="LS">Lesotho</option>
						<option value="LR">Liberia</option>
						<option value="LY">Libya</option>
						<option value="LI">Liechtenstein</option>
						<option value="LT">Lithuania</option>
						<option value="LU">Luxembourg</option>
						<option value="MO">Macao</option>
						<option value="MK">Macedonia, the former Yugoslav Republic of</option>
						<option value="MG">Madagascar</option>
						<option value="MW">Malawi</option>
						<option value="MY">Malaysia</option>
						<option value="MV">Maldives</option>
						<option value="ML">Mali</option>
						<option value="MT">Malta</option>
						<option value="MH">Marshall Islands</option>
						<option value="MQ">Martinique</option>
						<option value="MR">Mauritania</option>
						<option value="MU">Mauritius</option>
						<option value="YT">Mayotte</option>
						<option value="MX">Mexico</option>
						<option value="FM">Micronesia (Federated States of)</option>
						<option value="MD">Moldova, Republic of</option>
						<option value="MC">Monaco</option>
						<option value="MN">Mongolia</option>
						<option value="ME">Montenegro</option>
						<option value="MS">Montserrat</option>
						<option value="MA">Morocco</option>
						<option value="MZ">Mozambique</option>
						<option value="MM">Myanmar</option>
						<option value="NA">Namibia</option>
						<option value="NR">Nauru</option>
						<option value="NP">Nepal</option>
						<option value="NL">Netherlands</option>
						<option value="NC">New Caledonia</option>
						<option value="NZ">New Zealand</option>
						<option value="NI">Nicaragua</option>
						<option value="NE">Niger</option>
						<option value="NG">Nigeria</option>
						<option value="NU">Niue</option>
						<option value="NF">Norfolk Island</option>
						<option value="MP">Northern Mariana Islands</option>
						<option value="NO">Norway</option>
						<option value="OM">Oman</option>
						<option value="PK">Pakistan</option>
						<option value="PW">Palau</option>
						<option value="PS">Palestine, State of</option>
						<option value="PA">Panama</option>
						<option value="PG">Papua New Guinea</option>
						<option value="PY">Paraguay</option>
						<option value="PE">Peru</option>
						<option value="PH">Philippines</option>
						<option value="PN">Pitcairn</option>
						<option value="PL">Poland</option>
						<option value="PT">Portugal</option>
						<option value="PR">Puerto Rico</option>
						<option value="QA">Qatar</option>
						<option value="RE">Réunion</option>
						<option value="RO">Romania</option>
						<option value="RU">Russian Federation</option>
						<option value="RW">Rwanda</option>
						<option value="BL">Saint Barthélemy</option>
						<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
						<option value="KN">Saint Kitts and Nevis</option>
						<option value="LC">Saint Lucia</option>
						<option value="MF">Saint Martin (French part)</option>
						<option value="PM">Saint Pierre and Miquelon</option>
						<option value="VC">Saint Vincent and the Grenadines</option>
						<option value="WS">Samoa</option>
						<option value="SM">San Marino</option>
						<option value="ST">Sao Tome and Principe</option>
						<option value="SA">Saudi Arabia</option>
						<option value="SN">Senegal</option>
						<option value="RS">Serbia</option>
						<option value="SC">Seychelles</option>
						<option value="SL">Sierra Leone</option>
						<option value="SG">Singapore</option>
						<option value="SX">Sint Maarten (Dutch part)</option>
						<option value="SK">Slovakia</option>
						<option value="SI">Slovenia</option>
						<option value="SB">Solomon Islands</option>
						<option value="SO">Somalia</option>
						<option value="ZA">South Africa</option>
						<option value="GS">South Georgia and the South Sandwich Islands</option>
						<option value="SS">South Sudan</option>
						<option value="ES">Spain</option>
						<option value="LK">Sri Lanka</option>
						<option value="SD">Sudan</option>
						<option value="SR">Suriname</option>
						<option value="SJ">Svalbard and Jan Mayen</option>
						<option value="SE">Sweden</option>
						<option value="CH">Switzerland</option>
						<option value="SY">Syrian Arab Republic</option>
						<option value="TW">Taiwan, Province of China</option>
						<option value="TJ">Tajikistan</option>
						<option value="TZ">Tanzania, United Republic of</option>
						<option value="TH">Thailand</option>
						<option value="TL">Timor-Leste</option>
						<option value="TG">Togo</option>
						<option value="TK">Tokelau</option>
						<option value="TO">Tonga</option>
						<option value="TT">Trinidad and Tobago</option>
						<option value="TN">Tunisia</option>
						<option value="TR">Turkey</option>
						<option value="TM">Turkmenistan</option>
						<option value="TC">Turks and Caicos Islands</option>
						<option value="TV">Tuvalu</option>
						<option value="UG">Uganda</option>
						<option value="UA">Ukraine</option>
						<option value="AE">United Arab Emirates</option>
						<option value="GB">United Kingdom of Great Britain and Northern Ireland</option>
						<option value="UM">United States Minor Outlying Islands</option>
						<option value="US" selected="selected">United States of America</option>
						<option value="UY">Uruguay</option>
						<option value="UZ">Uzbekistan</option>
						<option value="VU">Vanuatu</option>
						<option value="VE">Venezuela (Bolivarian Republic of)</option>
						<option value="VN">Viet Nam</option>
						<option value="VG">Virgin Islands (British)</option>
						<option value="VI">Virgin Islands (U.S.)</option>
						<option value="WF">Wallis and Futuna</option>
						<option value="EH">Western Sahara</option>
						<option value="YE">Yemen</option>
						<option value="ZM">Zambia</option>
						<option value="ZW">Zimbabwe</option>
					</select>
				</td>
			</tr>
			<tr style="height: 35px;">
				<td width="15%" align="right">
					<label>Location:</label>
				</td>
				<td width="*">
					<input id="location" name="location" style="width:100%" class="k-content"/>
				</td>
			</tr>
		</table>

	</div>

	<div id="myMap"></div>
</body>
</html>

Full Source Code


The complete source code can be found at https://github.com/GregoryAlexander77/creating-an-azure-maps-autocomplete-widget-with-kendo-ui/blob/main/code

Please consider starring this repo if you find this article useful.


Further Reading