Tutorial: Use Azure Maps to create a store locator
This tutorial guides you through the process of creating a simple store locator using Azure Maps. In this tutorial, you'll learn how to:
- Create a new webpage by using the Azure Map Control API.
- Load custom data from a file and display it on a map.
- Use the Azure Maps Search service to find an address or enter a query.
- Get the user's location from the browser and show it on the map.
- Combine multiple layers to create custom symbols on the map.
- Cluster data points.
- Add zoom controls to the map.
Prerequisites
- Make an Azure Maps account in Gen 1 (S1) or Gen 2 pricing tier.
- Obtain a primary subscription key, also known as the primary key or the subscription key.
For more information about Azure Maps authentication, see manage authentication in Azure Maps.
This tutorial uses the Visual Studio Code application, but you can use a different coding environment.
Sample code
In this tutorial, we'll create a store locator for a fictional company called Contoso Coffee. Also, the tutorial includes some tips to help you learn about extending the store locator with other optional functionalities.
You can view the Live store locator sample here.
To more easily follow and engage this tutorial, you'll need to download the following resources:
- Full source code for simple store locator sample
- Store location data to import into the store locator dataset
- Map images
Store locator features
This section lists the features that are supported in the Contoso Coffee store locator application.
User interface features
- Store logo on the header
- Map supports panning and zooming
- A My Location button to search over the user's current location.
- Page layout adjusts based on the width of the device screen
- A search box and a search button
Functionality features
- A
keypressevent added to the search box triggers a search when the user presses Enter. - When the map moves, the distance to each location from the center of the map calculates. The results list updates to display the closest locations at the top of the map.
- When the user selects a result in the results list, the map is centered over the selected location and information about the location appears in a pop-up window.
- When the user selects a specific location, the map triggers a pop-up window.
- When the user zooms out, locations are grouped in clusters. Each cluster is represented by a circle with a number inside the circle. Clusters form and separate as the user changes the zoom level.
- Selecting a cluster zooms in two levels on the map and centers over the location of the cluster.
Store locator design
The following figure shows a wireframe of the general layout of our store locator. You can view the live wireframe here.
To maximize the usefulness of this store locator, we include a responsive layout that adjusts when a user's screen width is smaller than 700 pixels wide. A responsive layout makes it easy to use the store locator on a small screen, like on a mobile device. Here's a wireframe of the small-screen layout:
Create the store location dataset
This section describes how to create a dataset of the stores that you want to display on the map. The dataset for the Contoso Coffee locator is created inside an Excel workbook. The dataset contains 10,213 Contoso Coffee coffee shop locations spread across nine countries or regions: the United States, Canada, the United Kingdom, France, Germany, Italy, the Netherlands, Denmark, and Spain. Here's a screenshot of what the data looks like:
To view the full dataset, download the Excel workbook here.
Looking at the screenshot of the data, we can make the following observations:
- Location information is stored by using the AddressLine, City, Municipality (county), AdminDivision (state/province), PostCode (postal code), and Country columns.
- The Latitude and Longitude columns contain the coordinates for each Contoso Coffee location. If you don't have coordinates information, you can use the Search services in Azure Maps to determine the location coordinates.
- Some other columns contain metadata that's related to the coffee shops: a phone number, Boolean columns, and store opening and closing times in 24-hour format. The Boolean columns are for Wi-Fi and wheelchair accessibility. You can create your own columns that contain metadata that's more relevant to your location data.
Note
Azure Maps renders data in the spherical Mercator projection "EPSG:3857" but reads data in "EPSG:4326" that use the WGS84 datum.
Load the store location dataset
The Contoso Coffee shop locator dataset is small, so we'll convert the Excel worksheet into a tab-delimited text file. This file can then be downloaded by the browser when the application loads.
Tip
If your dataset is too large for client download, or is updated frequently, you might consider storing your dataset in a database. After your data is loaded into a database, you can then set up a web service that accepts queries for the data, and then sends the results to the user's browser.
Convert data to tab-delimited text file
To convert the Contoso Coffee shop location data from an Excel workbook into a flat text file:
Save the workbook to your hard drive.
Load the Excel app.
Open the downloaded workbook.
Select Save As.
In the Save as type drop-down list, select Text (Tab delimited)(*.txt).
Name the file ContosoCoffee.
If you open the text file in Notepad, it looks similar to the following text:
Set up the project
Open the Visual Studio Code app.
Select File, and then select Open Workspace....
Create a new folder and name it "ContosoCoffee".
Select CONTOSOCOFFEE in the explorer.
Create the following three files that define the layout, style, and logic for the application:
- index.html
- index.css
- index.js
Create a folder named data.
Add ContosoCoffee.txt to the data folder.
Create another folder named images.
If you haven't already, download these 10 images.
Add the downloaded images to the images folder.
Your workspace folder should now look like the following screenshot:
Create the HTML
To create the HTML:
Add the following
metatags to theheadof index.html:<meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="IE=Edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">Add references to the Azure Maps web control JavaScript and CSS files:
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css"> <script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>Add a reference to the Azure Maps Services module. The module is a JavaScript library that wraps the Azure Maps REST services and makes them easy to use in JavaScript. The module is useful for powering search functionality.
<script src="https://atlas.microsoft.com/sdk/javascript/service/2/atlas-service.min.js"></script>Add references to index.js and index.css.
<link rel="stylesheet" href="index.css" type="text/css"> <script src="index.js"></script>In the body of the document, add a
headertag. Inside theheadertag, add the logo and company name.<header> <img src="images/Logo.png" /> <span>Contoso Coffee</span> </header>Add a
maintag and create a search panel that has a text box and search button. Also, adddivreferences for the map, the list panel, and the My Location GPS button.<main> <div class="searchPanel"> <div> <input id="searchTbx" type="search" placeholder="Find a store" /> <button id="searchBtn" title="Search"></button> </div> </div> <div id="listPanel"></div> <div id="myMap"></div> <button id="myLocationBtn" title="My Location"></button> </main>
After you finish, index.html should look like this example index.html file.
Define the CSS Styles
The next step is to define the CSS styles. CSS styles define how the application components are laid out and the application's appearance.
Open index.css.
Add the following css code:
Note
The
@mediastyle defines alternate style options to use when the screen width is smaller than 700 pixels.html, body { padding: 0; margin: 0; font-family: Gotham, Helvetica, sans-serif; overflow-x: hidden; } header { width: calc(100vw - 10px); height: 30px; padding: 15px 0 20px 20px; font-size: 25px; font-style: italic; font-family: "Comic Sans MS", cursive, sans-serif; line-height: 30px; font-weight: bold; color: white; background-color: #007faa; } header span { vertical-align: middle; } header img { height: 30px; vertical-align: middle; } .searchPanel { position: relative; width: 350px; } .searchPanel div { padding: 20px; } .searchPanel input { width: calc(100% - 50px); font-size: 16px; border: 0; border-bottom: 1px solid #ccc; } #listPanel { position: absolute; top: 135px; left: 0px; width: 350px; height: calc(100vh - 135px); overflow-y: auto; } #myMap { position: absolute; top: 65px; left: 350px; width: calc(100vw - 350px); height: calc(100vh - 65px); } .statusMessage { margin: 10px; } #myLocationBtn, #searchBtn { margin: 0; padding: 0; border: none; border-collapse: collapse; width: 32px; height: 32px; text-align: center; cursor: pointer; line-height: 32px; background-repeat: no-repeat; background-size: 20px; background-position: center center; z-index: 200; } #myLocationBtn { position: absolute; top: 150px; right: 10px; box-shadow: 0px 0px 4px rgba(0,0,0,0.16); background-color: white; background-image: url("images/GpsIcon.png"); } #myLocationBtn:hover { background-image: url("images/GpsIcon-hover.png"); } #searchBtn { background-color: transparent; background-image: url("images/SearchIcon.png"); } #searchBtn:hover { background-image: url("images/SearchIcon-hover.png"); } .listItem { height: 50px; padding: 20px; font-size: 14px; } .listItem:hover { cursor: pointer; background-color: #f1f1f1; } .listItem-title { color: #007faa; font-weight: bold; } .storePopup { min-width: 150px; } .storePopup .popupTitle { border-top-left-radius: 4px; border-top-right-radius: 4px; padding: 8px; height: 30px; background-color: #007faa; color: white; font-weight: bold; } .storePopup .popupSubTitle { font-size: 10px; line-height: 12px; } .storePopup .popupContent { font-size: 11px; line-height: 18px; padding: 8px; } .storePopup img { vertical-align:middle; height: 12px; margin-right: 5px; } /* Adjust the layout of the page when the screen width is fewer than 700 pixels. */ @media screen and (max-width: 700px) { .searchPanel { width: 100vw; } #listPanel { top: 385px; width: 100%; height: calc(100vh - 385px); } #myMap { width: 100vw; height: 250px; top: 135px; left: 0px; } #myLocationBtn { top: 220px; } } .mapCenterIcon { display: block; width: 10px; height: 10px; border-radius: 50%; background: orange; border: 2px solid white; cursor: pointer; box-shadow: 0 0 0 rgba(0, 204, 255, 0.4); animation: pulse 3s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.4); } 70% { box-shadow: 0 0 0 50px rgba(0, 204, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 204, 255, 0); } }
Run the application. You'll see the header, search box, and search button. However, the map isn't visible because it hasn't been loaded yet. If you try to do a search, nothing happens. We need to set up the JavaScript logic, which is described in the next section. This logic accesses all the functionality of the store locator.
Add JavaScript code
The JavaScript code in the Contoso Coffee shop locator app enables the following processes:
Adds an event listener called
readyto wait until the page has completed its loading process. When the page loading is complete, the event handler creates more event listeners to monitor the loading of the map, and give functionality to the search and My location buttons.When the user selects the search button, or types a location in the search box then presses enter, a fuzzy search against the user's query is started. The code passes in an array of country/region ISO 2 values to the
countrySetoption to limit the search results to those countries/regions. Limiting the countries/regions to search helps increase the accuracy of the results that are returned.Once the search is finished, the first location result is used as the center focus of the map camera. When the user selects the My Location button, the code retrieves the user's location using the HTML5 Geolocation API that's built into the browser. After retrieving the location, the code centers the map over the user's location.
To add the JavaScript:
Open index.js.
Add global options to make settings easier to update. Define the variables for the map, pop up window, data source, icon layer, and HTML marker. Set the HTML marker to indicate the center of a search area. And, define an instance of the Azure Maps search service client.
//The maximum zoom level to cluster data point data on the map. var maxClusterZoomLevel = 11; //The URL to the store location data. var storeLocationDataUrl = 'data/ContosoCoffee.txt'; //The URL to the icon image. var iconImageUrl = 'images/CoffeeIcon.png'; var map, popup, datasource, iconLayer, centerMarker, searchURL;Add the following initialization code. Make sure to replace
<Your Azure Maps Key>with your primary subscription key.Tip
When you use pop-up windows, it's best to create a single
Popupinstance and reuse the instance by updating its content and position. For everyPopupinstance you add to your code, multiple DOM elements are added to the page. The more DOM elements there are on a page, the more things the browser has to keep track of. If there are too many items, the browser might become slow.function initialize() { //Initialize a map instance. map = new atlas.Map('myMap', { center: [-90, 40], zoom: 2, //Add your Azure Maps primary subscription key to the map SDK. authOptions: { authType: 'subscriptionKey', subscriptionKey: '<Your Azure Maps Key>' } }); //Create a pop-up window, but leave it closed so we can update it and display it later. popup = new atlas.Popup(); //Use SubscriptionKeyCredential with a subscription key const subscriptionKeyCredential = new atlas.service.SubscriptionKeyCredential(atlas.getSubscriptionKey()); //Use subscriptionKeyCredential to create a pipeline const pipeline = atlas.service.MapsURL.newPipeline(subscriptionKeyCredential, { retryOptions: { maxTries: 4 } // Retry options }); //Create an instance of the SearchURL client. searchURL = new atlas.service.SearchURL(pipeline); //If the user selects the search button, geocode the value the user passed in. document.getElementById('searchBtn').onclick = performSearch; //If the user presses Enter in the search box, perform a search. document.getElementById('searchTbx').onkeyup = function(e) { if (e.keyCode === 13) { performSearch(); } }; //If the user selects the My Location button, use the Geolocation API (Preview) to get the user's location. Center and zoom the map on that location. document.getElementById('myLocationBtn').onclick = setMapToUserLocation; //Wait until the map resources are ready. map.events.add('ready', function() { //Add your post-map load functionality. }); } //Create an array of country/region ISO 2 values to limit searches to. var countrySet = ['US', 'CA', 'GB', 'FR','DE','IT','ES','NL','DK']; function performSearch() { var query = document.getElementById('searchTbx').value; //Perform a fuzzy search on the users query. searchURL.searchFuzzy(atlas.service.Aborter.timeout(3000), query, { //Pass in the array of country/region ISO2 for which we want to limit the search to. countrySet: countrySet }).then(results => { //Parse the response into GeoJSON so that the map can understand. var data = results.geojson.getFeatures(); if (data.features.length > 0) { //Set the camera to the bounds of the results. map.setCamera({ bounds: data.features[0].bbox, padding: 40 }); } else { document.getElementById('listPanel').innerHTML = '<div class="statusMessage">Unable to find the location you searched for.</div>'; } }); } function setMapToUserLocation() { //Request the user's location. navigator.geolocation.getCurrentPosition(function(position) { //Convert the Geolocation API (Preview) position to a longitude and latitude position value that the map can interpret and center the map over it. map.setCamera({ center: [position.coords.longitude, position.coords.latitude], zoom: maxClusterZoomLevel + 1 }); }, function(error) { //If an error occurs when the API tries to access the user's position information, display an error message. switch (error.code) { case error.PERMISSION_DENIED: alert('User denied the request for geolocation.'); break; case error.POSITION_UNAVAILABLE: alert('Position information is unavailable.'); break; case error.TIMEOUT: alert('The request to get user position timed out.'); break; case error.UNKNOWN_ERROR: alert('An unknown error occurred.'); break; } }); } //Initialize the application when the page is loaded. window.onload = initialize;In the map's
readyevent listener, add a zoom control and an HTML marker to display the center of a search area.//Add a zoom control to the map. map.controls.add(new atlas.control.ZoomControl(), { position: 'top-right' }); //Add an HTML marker to the map to indicate the center to use for searching. centerMarker = new atlas.HtmlMarker({ htmlContent: '<div class="mapCenterIcon"></div>', position: map.getCamera().center }); map.markers.add(centerMarker);In the map's
readyevent listener, add a data source. Then, make a call to load and parse the dataset. Enable clustering on the data source. Clustering on the data source groups overlapping points together in a cluster. As the user zooms in, the clusters separate into individual points. This behavior provides a better user experience and improves performance.//Create a data source, add it to the map, and then enable clustering. datasource = new atlas.source.DataSource(null, { cluster: true, clusterMaxZoom: maxClusterZoomLevel - 1 }); map.sources.add(datasource); //Load all the store data now that the data source is defined. loadStoreData();After the dataset loads in the map's
readyevent listener, define a set of layers to render the data. A bubble layer renders clustered data points. A symbol layer renders the number of points in each cluster above the bubble layer. A second symbol layer renders a custom icon for individual locations on the map.Add
mouseoverandmouseoutevents to the bubble and icon layers to change the mouse cursor when the user hovers over a cluster or icon on the map. Add aclickevent to the cluster bubble layer. Thisclickevent zooms in the map two levels and centers the map over a cluster when the user selects any cluster. Add aclickevent to the icon layer. Thisclickevent displays a pop-up window that shows the details of a coffee shop when a user selects an individual location icon. Add an event to the map to monitor when the map is finished moving. When this event fires, update the items in the list panel.//Create a bubble layer to render clustered data points. var clusterBubbleLayer = new atlas.layer.BubbleLayer(datasource, null, { radius: 12, color: '#007faa', strokeColor: 'white', strokeWidth: 2, filter: ['has', 'point_count'] //Only render data points that have a point_count property; clusters have this property. }); //Create a symbol layer to render the count of locations in a cluster. var clusterLabelLayer = new atlas.layer.SymbolLayer(datasource, null, { iconOptions: { image: 'none' //Hide the icon image. }, textOptions: { textField: ['get', 'point_count_abbreviated'], size: 12, font: ['StandardFont-Bold'], offset: [0, 0.4], color: 'white' } }); map.layers.add([clusterBubbleLayer, clusterLabelLayer]); //Load a custom image icon into the map resources. map.imageSprite.add('myCustomIcon', iconImageUrl).then(function() { //Create a layer to render a coffee cup symbol above each bubble for an individual location. iconLayer = new atlas.layer.SymbolLayer(datasource, null, { iconOptions: { //Pass in the ID of the custom icon that was loaded into the map resources. image: 'myCustomIcon', //Optionally, scale the size of the icon. font: ['SegoeUi-Bold'], //Anchor the center of the icon image to the coordinate. anchor: 'center', //Allow the icons to overlap. allowOverlap: true }, filter: ['!', ['has', 'point_count']] //Filter out clustered points from this layer. }); map.layers.add(iconLayer); //When the mouse is over the cluster and icon layers, change the cursor to a pointer. map.events.add('mouseover', [clusterBubbleLayer, iconLayer], function() { map.getCanvasContainer().style.cursor = 'pointer'; }); //When the mouse leaves the item on the cluster and icon layers, change the cursor back to the default (grab). map.events.add('mouseout', [clusterBubbleLayer, iconLayer], function() { map.getCanvasContainer().style.cursor = 'grab'; }); //Add a click event to the cluster layer. When the user selects a cluster, zoom into it by two levels. map.events.add('click', clusterBubbleLayer, function(e) { map.setCamera({ center: e.position, zoom: map.getCamera().zoom + 2 }); }); //Add a click event to the icon layer and show the shape that was selected. map.events.add('click', iconLayer, function(e) { showPopup(e.shapes[0]); }); //Add an event to monitor when the map is finished rendering the map after it has moved. map.events.add('render', function() { //Update the data in the list. updateListItems(); }); });When the coffee shop dataset is loaded, it must first be downloaded. Then, the text file must be split into lines. The first line contains the header information. To make the code easier to follow, we parse the header into an object, which we can then use to look up the cell index of each property. After the first line, loop through the remaining lines and create a point feature. Add the point feature to the data source. Finally, update the list panel.
function loadStoreData() { //Download the store location data. fetch(storeLocationDataUrl) .then(response => response.text()) .then(function(text) { //Parse the tab-delimited file data into GeoJSON features. var features = []; //Split the lines of the file. var lines = text.split('\n'); //Grab the header row. var row = lines[0].split('\t'); //Parse the header row and index each column to make the code for parsing each row easier to follow. var header = {}; var numColumns = row.length; for (var i = 0; i < row.length; i++) { header[row[i]] = i; } //Skip the header row and then parse each row into a GeoJSON feature. for (var i = 1; i < lines.length; i++) { row = lines[i].split('\t'); //Ensure that the row has the correct number of columns. if (row.length >= numColumns) { features.push(new atlas.data.Feature(new atlas.data.Point([parseFloat(row[header['Longitude']]), parseFloat(row[header['Latitude']])]), { AddressLine: row[header['AddressLine']], City: row[header['City']], Municipality: row[header['Municipality']], AdminDivision: row[header['AdminDivision']], Country: row[header['Country']], PostCode: row[header['PostCode']], Phone: row[header['Phone']], StoreType: row[header['StoreType']], IsWiFiHotSpot: (row[header['IsWiFiHotSpot']].toLowerCase() === 'true') ? true : false, IsWheelchairAccessible: (row[header['IsWheelchairAccessible']].toLowerCase() === 'true') ? true : false, Opens: parseInt(row[header['Opens']]), Closes: parseInt(row[header['Closes']]) })); } } //Add the features to the data source. datasource.add(new atlas.data.FeatureCollection(features)); //Initially, update the list items. updateListItems(); }); }When the list panel is updated, the distance is calculated. This distance is from the center of the map to all point features in the current map view. The features are then sorted by distance. HTML is generated to display each location in the list panel.
var listItemTemplate = '<div class="listItem" onclick="itemSelected(\'{id}\')"><div class="listItem-title">{title}</div>{city}<br />Open until {closes}<br />{distance} miles away</div>'; function updateListItems() { //Hide the center marker. centerMarker.setOptions({ visible: false }); //Get the current camera and view information for the map. var camera = map.getCamera(); var listPanel = document.getElementById('listPanel'); //Check to see whether the user is zoomed out a substantial distance. If they are, tell the user to zoom in and to perform a search or select the My Location button. if (camera.zoom < maxClusterZoomLevel) { //Close the pop-up window; clusters might be displayed on the map. popup.close(); listPanel.innerHTML = '<div class="statusMessage">Search for a location, zoom the map, or select the My Location button to see individual locations.</div>'; } else { //Update the location of the centerMarker property. centerMarker.setOptions({ position: camera.center, visible: true }); //List the ten closest locations in the side panel. var html = [], properties; /* Generating HTML for each item that looks like this: <div class="listItem" onclick="itemSelected('id')"> <div class="listItem-title">1 Microsoft Way</div> Redmond, WA 98052<br /> Open until 9:00 PM<br /> 0.7 miles away </div> */ //Get all the shapes that have been rendered in the bubble layer. var data = map.layers.getRenderedShapes(map.getCamera().bounds, [iconLayer]); //Create an index of the distances of each shape. var distances = {}; data.forEach(function (shape) { if (shape instanceof atlas.Shape) { //Calculate the distance from the center of the map to each shape and store in the index. Round to 2 decimals. distances[shape.getId()] = Math.round(atlas.math.getDistanceTo(camera.center, shape.getCoordinates(), 'miles') * 100) / 100; } }); //Sort the data by distance. data.sort(function (x, y) { return distances[x.getId()] - distances[y.getId()]; }); data.forEach(function(shape) { properties = shape.getProperties(); html.push('<div class="listItem" onclick="itemSelected(\'', shape.getId(), '\')"><div class="listItem-title">', properties['AddressLine'], '</div>', //Get a formatted addressLine2 value that consists of City, Municipality, AdminDivision, and PostCode. getAddressLine2(properties), '<br />', //Convert the closing time to a format that is easier to read. getOpenTillTime(properties), '<br />', //Get the distance of the shape. distances[shape.getId()], ' miles away</div>'); }); listPanel.innerHTML = html.join(''); //Scroll to the top of the list panel in case the user has scrolled down. listPanel.scrollTop = 0; } } //This converts a time that's in a 24-hour format to an AM/PM time or noon/midnight string. function getOpenTillTime(properties) { var time = properties['Closes']; var t = time / 100; var sTime; if (time === 1200) { sTime = 'noon'; } else if (time === 0 || time === 2400) { sTime = 'midnight'; } else { sTime = Math.round(t) + ':'; //Get the minutes. t = (t - Math.round(t)) * 100; if (t === 0) { sTime += '00'; } else if (t < 10) { sTime += '0' + t; } else { sTime += Math.round(t); } if (time < 1200) { sTime += ' AM'; } else { sTime += ' PM'; } } return 'Open until ' + sTime; } //Create an addressLine2 string that contains City, Municipality, AdminDivision, and PostCode. function getAddressLine2(properties) { var html = [properties['City']]; if (properties['Municipality']) { html.push(', ', properties['Municipality']); } if (properties['AdminDivision']) { html.push(', ', properties['AdminDivision']); } if (properties['PostCode']) { html.push(' ', properties['PostCode']); } return html.join(''); }When the user selects an item in the list panel, the shape to which the item is related is retrieved from the data source. A pop-up window is generated that's based on the property information stored in the shape. The map centers over the shape. If the map is fewer than 700 pixels wide, the map view is offset so the pop-up window is visible.
//When a user selects a result in the side panel, look up the shape by its ID value and display the pop-up window. function itemSelected(id) { //Get the shape from the data source by using its ID. var shape = datasource.getShapeById(id); showPopup(shape); //Center the map over the shape on the map. var center = shape.getCoordinates(); var offset; //If the map is fewer than 700 pixels wide, then the layout is set for small screens. if (map.getCanvas().width < 700) { //When the map is small, offset the center of the map relative to the shape so that there is room for the popup to appear. offset = [0, -80]; } map.setCamera({ center: center, centerOffset: offset }); } function showPopup(shape) { var properties = shape.getProperties(); /* Generating HTML for the pop-up window that looks like this: <div class="storePopup"> <div class="popupTitle"> 3159 Tongass Avenue <div class="popupSubTitle">Ketchikan, AK 99901</div> </div> <div class="popupContent"> Open until 22:00 PM<br/> <img title="Phone Icon" src="images/PhoneIcon.png"> <a href="tel:1-800-XXX-XXXX">1-800-XXX-XXXX</a> <br>Amenities: <img title="Wi-Fi Hotspot" src="images/WiFiIcon.png"> <img title="Wheelchair Accessible" src="images/WheelChair-small.png"> </div> </div> */ //Calculate the distance from the center of the map to the shape in miles, round to 2 decimals. var distance = Math.round(atlas.math.getDistanceTo(map.getCamera().center, shape.getCoordinates(), 'miles') * 100)/100; var html = ['<div class="storePopup">']; html.push('<div class="popupTitle">', properties['AddressLine'], '<div class="popupSubTitle">', getAddressLine2(properties), '</div></div><div class="popupContent">', //Convert the closing time to a format that's easier to read. getOpenTillTime(properties), //Add the distance information. '<br/>', distance, ' miles away', '<br /><img src="images/PhoneIcon.png" title="Phone Icon"/><a href="tel:', properties['Phone'], '">', properties['Phone'], '</a>' ); if (properties['IsWiFiHotSpot'] || properties['IsWheelchairAccessible']) { html.push('<br/>Amenities: '); if (properties['IsWiFiHotSpot']) { html.push('<img src="images/WiFiIcon.png" title="Wi-Fi Hotspot"/>'); } if (properties['IsWheelchairAccessible']) { html.push('<img src="images/WheelChair-small.png" title="Wheelchair Accessible"/>'); } } html.push('</div></div>'); //Update the content and position of the pop-up window for the specified shape information. popup.setOptions({ //Create a table from the properties in the feature. content: html.join(''), position: shape.getCoordinates() }); //Open the pop-up window. popup.open(map); }
Now, you have a fully functional store locator. In a web browser, open the index.html file for the store locator. When the clusters appear on the map, you can search for a location by using the search box, by selecting the My Location button, by selecting a cluster, or by zooming in on the map to see individual locations.
The first time a user selects the My Location button, the browser displays a security warning that asks for permission to access the user's location. If the user agrees to share their location, the map zooms in on the user's location, and nearby coffee shops are shown.

When you zoom in close enough in an area that has coffee shop locations, the clusters separate into individual locations. Select one of the icons on the map or select an item in the side panel to see a pop-up window. The pop-up shows information for the selected location.

If you resize the browser window to fewer than 700 pixels wide or open the application on a mobile device, the layout changes to be better suited for smaller screens.

In this tutorial, you learned how to create a basic store locator by using Azure Maps. The store locator you create in this tutorial might have all the functionality you need. You can add features to your store locator or use more advance features for a more custom user experience:
- Enable suggestions as you type in the search box.
- Add support for multiple languages.
- Allow the user to filter locations along a route.
- Add the ability to set filters.
- Add support to specify an initial search value by using a query string. When you include this option in your store locator, users are then able to bookmark and share searches. It also provides an easy method for you to pass searches to this page from another page.
- Deploy your store locator as an Azure App Service Web App.
- Store your data in a database and search for nearby locations. To learn more, see the SQL Server spatial data types overview and Query spatial data for the nearest neighbor.
You can view full source code here. View the live sample and learn more about the coverage and capabilities of Azure Maps by using Zoom levels and tile grid. You can also Use data-driven style expressions to apply to your business logic.
Clean up resources
There are no resources that require cleanup.
Next steps
To see more code examples and an interactive coding experience: