Creación de un localizador de almacén mediante Azure MapsCreate a store locator by using Azure Maps

Este tutorial le guía por el proceso de creación de un localizador de almacén sencillo mediante Azure Maps.This tutorial guides you through the process of creating a simple store locator by using Azure Maps. Los localizadores de almacén son comunes.Store locators are common. Muchos de los conceptos que se usan en este tipo de aplicación son aplicables a muchos otros tipos de aplicaciones.Many of the concepts that are used in this type of application are applicable to many other types of applications. Para la mayoría de las empresas que venden directamente a los consumidores, es imprescindible ofrecer a los clientes un localizador de almacén.Offering a store locator to customers is a must for most businesses that sell directly to consumers. En este tutorial, aprenderá a:In this tutorial, you learn how to:

  • Crear una página web mediante la API de Control de mapa de Azure.Create a new webpage by using the Azure Map Control API.
  • Cargar datos personalizados desde un archivo y mostrarlos en un mapa.Load custom data from a file and display it on a map.
  • Usar el servicio de búsqueda de Azure Maps para encontrar una dirección o escribir una consulta.Use the Azure Maps Search service to find an address or enter a query.
  • Obtener la ubicación del usuario desde el explorador y mostrarla en el mapa.Get the user's location from the browser and show it on the map.
  • Combinar varias capas para crear símbolos personalizados en el mapa.Combine multiple layers to create custom symbols on the map.
  • Agrupar puntos de datos.Cluster data points.
  • Agregar controles de zoom al mapa.Add zoom controls to the map.

Avance al ejemplo de localizador de almacén activo o al código fuente.Jump ahead to the live store locator example or source code.

Requisitos previosPrerequisites

Para realizar los pasos de este tutorial, primero debe crear su cuenta de Azure Maps y obtener la clave de suscripción de su cuenta.To complete the steps in this tutorial, you first need to create your Azure Maps account and get the subscription key for your account.

DiseñoDesign

Antes de empezar con el código, es una buena idea comenzar con un diseño.Before you jump into the code, it's a good idea to begin with a design. El localizador de almacén puede ser tan sencillo o complejo como se quiera.Your store locator can be as simple or complex as you want it to be. En este tutorial, crearemos un localizador de almacén sencillo.In this tutorial, we create a simple store locator. Se incluyen algunas sugerencias a lo largo del camino para ayudarle a ampliar algunas funcionalidades si elige hacerlo.We include some tips along the way to help you extend some functionalities if you choose to. Crearemos un localizador de almacén para una compañía ficticia llamada Contoso Coffee.We create a store locator for a fictional company called Contoso Coffee. En la siguiente ilustración se muestra un contorno reticular del diseño general del localizador de almacén que se va a crear en este tutorial:The following figure shows a wireframe of the general layout of the store locator we build in this tutorial:


Contorno reticular de un localizador de almacén para ubicaciones de cafetería de Contoso Coffee

Para sacar el máximo provecho de este localizador de almacén, se incluye un diseño dinámico que se ajusta cuando el ancho de pantalla de un usuario es inferior a 700 píxeles.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. Un diseño dinámico facilita el uso del localizador de almacén en una pantalla pequeña, como la de un dispositivo móvil.A responsive layout makes it easy to use the store locator on a small screen, like on a mobile device. Este es el contorno reticular de un diseño de pantalla pequeña:Here's a wireframe of a small-screen layout:


Contorno reticular del localizador de almacén de Contoso Coffee en un dispositivo móvil

Los contornos reticulares muestran una aplicación bastante sencilla.The wireframes show a fairly straightforward application. La aplicación tiene un cuadro de búsqueda, una lista de tiendas cercanas, un mapa que tiene algunos marcadores (símbolos) y una ventana emergente que muestra información adicional cuando el usuario selecciona un marcador.The application has a search box, a list of nearby stores, a map that has some markers (symbols), and a pop-up window that displays additional information when the user selects a marker. Con más detalle, estas son las características que se van a crear en este localizador de almacén en este tutorial:In more detail, here are the features we build into this store locator in this tutorial:

  • Todas las ubicaciones del archivo de datos importado delimitado por tabulaciones se cargan en el mapa.All locations from the imported tab-delimited data file are loaded on the map.
  • El usuario puede realizar una panorámica del mapa y acercarlo o alejarlo, efectuar una búsqueda y seleccionar el botón del GPS My Location (Mi ubicación).The user can pan and zoom the map, perform a search, and select the My Location GPS button.
  • El diseño de página se ajusta según el ancho de la pantalla del dispositivo.The page layout adjusts based on the width of the device screen.
  • Un encabezado muestra el logotipo de la tienda.A header shows the store logo.
  • El usuario puede usar un cuadro de búsqueda y un botón de búsqueda para buscar una ubicación, como una dirección, un código postal o una ciudad.The user can use a search box and search button to search for a location, such as an address, postal code, or city.
  • Un evento keypress agregado al cuadro de búsqueda desencadena una búsqueda si el usuario presiona Entrar.A keypress event added to the search box triggers a search if the user presses Enter. Esta funcionalidad a menudo se pasa por alto, pero crea una mejor experiencia de usuario.This functionality often is overlooked, but it creates a better user experience.
  • Cuando el mapa se mueve, se calcula la distancia a cada ubicación desde el centro del mapa.When the map moves, the distance to each location from the center of the map is calculated. La lista de resultados se actualiza para mostrar las ubicaciones más próximas en la parte superior del mapa.The results list is updated to display the closest locations at the top of the map.
  • Cuando se selecciona un resultado de la lista de resultados, el mapa se centra en la ubicación seleccionada y aparece información sobre la ubicación en una ventana emergente.When you select 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.
  • Al seleccionar una ubicación específica en el mapa, también se muestra una ventana emergente.Selecting a specific location on the map also triggers a pop-up window.
  • Cuando el usuario aleja el mapa, las ubicaciones se agrupan en clústeres.When the user zooms out, locations are grouped in clusters. Los clústeres se representan mediante un círculo con un número dentro.Clusters are represented by a circle with a number inside the circle. Los clústeres se forman y se separan a medida que el usuario cambia el nivel de zoom.Clusters form and separate as the user changes the zoom level.
  • Al seleccionar un clúster se acerca el mapa dos niveles y se centra sobre la ubicación del clúster.Selecting a cluster zooms in on the map two levels and centers over the location of the cluster.

Creación del conjunto de datos de la ubicación de almacénCreate the store location dataset

Antes de desarrollar una aplicación de localizador de almacén, se debe crear un conjunto de datos de las tiendas que quiere mostrar en el mapa.Before we develop a store locator application, we need to create a dataset of the stores we want to display on the map. En este tutorial, se usará un conjunto de datos de una cafetería ficticia llamada Contoso Coffee.In this tutorial, we use a dataset for a fictitious coffee shop called Contoso Coffee. El conjunto de datos de este localizador de almacén sencillo se administra en un libro de Excel.The dataset for this simple store locator is managed in an Excel workbook. El conjunto de datos contiene 10 213 ubicaciones de cafetería de Contoso Coffee repartidas alrededor de nueve países: Estados Unidos, Canadá, Reino Unido, Francia, Alemania, Italia, Países Bajos, Dinamarca y España.The dataset contains 10,213 Contoso Coffee coffee shop locations spread across nine countries: the United States, Canada, the United Kingdom, France, Germany, Italy, the Netherlands, Denmark, and Spain. Esta es una captura de pantalla del aspecto de los datos:Here's a screenshot of what the data looks like:


Captura de pantalla de los datos del localizador de almacén en un libro de Excel

Puede descargar el libro de Excel.You can download the Excel workbook.

Al examinar la captura de pantalla de los datos, podemos hacer las observaciones siguientes:Looking at the screenshot of the data, we can make the following observations:

  • La información de ubicación se almacena mediante las columnas AddressLine, City, Municipality (municipio), AdminDivision (región/provincia), PostCode (código postal) y Country.Location information is stored by using the AddressLine, City, Municipality (county), AdminDivision (state/province), PostCode (postal code), and Country columns.
  • Las columnas Latitude y Longitude contienen las coordenadas de cada ubicación de cafetería de Contoso Coffee.The Latitude and Longitude columns contain the coordinates for each Contoso Coffee coffee shop location. Si no tiene información de coordenadas, puede usar los servicios de búsqueda de Azure Maps para determinar las coordenadas de ubicación.If you don't have coordinates information, you can use the Search services in Azure Maps to determine the location coordinates.
  • Algunas columnas adicionales contienen metadatos relacionados con las cafeterías: un número de teléfono, columnas booleanas para zonas activas Wi-Fi y accesibilidad con silla de ruedas, y el horario de apertura y cierre de la tienda en formato de 24 horas.Some additional columns contain metadata related to the coffee shops: a phone number, Boolean columns for Wi-Fi hotspot and wheelchair accessibility, and store opening and closing times in 24-hour format. Puede crear sus propias columnas que contengan metadatos más significativos para sus datos de ubicación.You can create your own columns that contain metadata that’s more relevant to your location data.

Nota

Azure Maps representa los datos en la proyección esférica de Mercator "EPSG:3857" pero lee los datos en "EPSG:4325" que usan los datos de WGS84.Azure Maps renders data in the spherical Mercator projection "EPSG:3857" but reads data in "EPSG:4325" that use the WGS84 datum.

Hay muchas maneras de exponer el conjunto de datos a la aplicación.There are many ways to expose the dataset to the application. Un enfoque es cargar los datos en una base de datos y exponer un servicio web que consulte los datos y envíe los resultados al explorador del usuario.One approach is to load the data into a database and expose a web service that queries the data and sends the results to the user’s browser. Esta opción es muy conveniente para grandes conjuntos de datos o para conjuntos de datos que se actualizan con frecuencia.This option is ideal for large datasets or for datasets that are updated frequently. Sin embargo, esta opción requiere mucho más trabajo de desarrollo y el costo es mayor.However, this option requires significantly more development work and has a higher cost.

Otro enfoque consiste en convertir este conjunto de datos en un archivo de texto sin formato que el explorador pueda analizar fácilmente.Another approach is to convert this dataset into a flat text file that the browser can easily parse. El archivo en sí se puede hospedar con el resto de la aplicación.The file itself can be hosted with the rest of the application. Esta opción simplifica las cosas, pero solo es adecuada para conjuntos de datos más pequeños debido a que el usuario descarga todos los datos.This option keeps things simple, but it's a good option only for smaller datasets because the user downloads all the data. Con este conjunto de datos se usará el archivo de texto sin formato porque el tamaño del archivo de datos es inferior a 1 MB.We use the flat text file for this dataset because the data file size is smaller than 1 MB.

Para convertir el libro en un archivo de texto sin formato, guárdelo como un archivo delimitado por tabulaciones.To convert the workbook to a flat text file, save the workbook as a tab-delimited file. Cada columna está delimitada por un carácter de tabulación, lo que facilita el análisis de las columnas en nuestro código.Each column is delimited by a tab character, which makes the columns easy to parse in our code. Puede usar el formato de valores separados por comas (CSV), pero esa opción requiere más lógica de análisis.You could use comma-separated value (CSV) format, but that option requires more parsing logic. Cualquier campo que tenga una coma alrededor podría incluirse entre comillas.Any field that has a comma around it would be wrapped with quotation marks. Para exportar estos datos como un archivo delimitado por tabulaciones en Excel, seleccione Save As (Guardar como).To export this data as a tab-delimited file in Excel, select Save As. En la lista desplegable Save as type (Guardar como tipo), seleccione Text (Tab delimited)(*.txt) (Texto (delimitado por tabulaciones)[*.txt]).In the Save as type drop-down list, select Text (Tab delimited)(*.txt). Asigne al archivo el nombre ContosoCoffee.txt.Name the file ContosoCoffee.txt.


Captura de pantalla del cuadro de diálogo Guardar como tipo

Si abre el archivo de texto en el Bloc de notas, se parecerá a la siguiente ilustración:If you open the text file in Notepad, it looks similar to the following figure:


Captura de pantalla de un archivo de Bloc de notas que muestra un conjunto de datos delimitado por tabulaciones

Configuración del proyectoSet up the project

Para crear el proyecto, puede usar Visual Studio o el editor de código de su elección.To create the project, you can use Visual Studio or the code editor of your choice. En la carpeta del proyecto, cree tres archivos: index.html, index.css e index.js.In your project folder, create three files: index.html, index.css, and index.js. Estos archivos definen el diseño, el estilo y la lógica de la aplicación.These files define the layout, style, and logic for the application. Cree una carpeta llamada data y agregue a ella ContosoCoffee.txt.Create a folder named data and add ContosoCoffee.txt to the folder. Cree otra carpeta llamada images.Create another folder named images. Se usarán diez imágenes en esta aplicación para iconos, botones y marcadores en el mapa.We use ten images in this application for icons, buttons, and markers on the map. También puede descargar estas imágenes.You can download these images. La carpeta del proyecto ahora debería parecerse a la siguiente ilustración:Your project folder should now look like the following figure:


Captura de pantalla de la carpeta de proyecto del localizador de almacén sencillo

Creación de la interfaz de usuarioCreate the user interface

Para crear la interfaz de usuario, agregue código a index.html:To create the user interface, add code to index.html:

  1. Agregue las siguientes etiquetas meta a head de index.html.Add the following meta tags to the head of index.html. Las etiquetas definen el juego de caracteres (UTF-8), indican a Internet Explorer y Microsoft Edge que usen las versiones más recientes del explorador y especifican una ventanilla que funciona bien con diseños dinámicos.The tags define the character set (UTF-8), tell Internet Explorer and Microsoft Edge to use the latest browser versions, and specify a viewport that works well for responsive layouts.

    <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">
    
  2. Agregue referencias a los archivos CSS y JavaScript de control web de Azure Maps: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>
    
  3. Agregue una referencia al módulo de servicios de Azure Maps.Add a reference to the Azure Maps Services module. El módulo es una biblioteca de JavaScript que contiene los servicios REST de Azure Maps y facilita su uso en JavaScript.The module is a JavaScript library that wraps the Azure Maps REST services and makes them easy to use in JavaScript. Resulta útil para activar la funcionalidad de búsqueda.The module is useful for powering search functionality.

    <script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas-service.min.js"></script>
    
  4. Agregue referencias a index.js e index.css:Add references to index.js and index.css:

    <link rel="stylesheet" href="index.css" type="text/css">
    <script src="index.js"></script>
    
  5. En el cuerpo del documento, agregue una etiqueta header.In the body of the document, add a header tag. Dentro de la etiqueta header, agregue el logotipo y el nombre de la empresa.Inside the header tag, add the logo and company name.

    <header>
        <img src="images/Logo.png" />
        <span>Contoso Coffee</span>
    </header>
    
  6. Agregue una etiqueta main y cree un panel de búsqueda que tenga un cuadro de texto y un botón de búsqueda.Add a main tag and create a search panel that has a text box and search button. Además, agregue referencias div al mapa, el panel de lista y el botón de GPS My Location (Mi ubicación).Also, add div references 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>
    

Cuando haya terminado, index.html se parecerá a este archivo index.html de ejemplo.When you're finished, index.html should look like this example index.html file.

El siguiente paso consiste en definir los estilos CSS.The next step is to define the CSS styles. Los estilos CSS definen cómo se distribuyen los componentes de aplicación y la apariencia de la aplicación.CSS styles define how the application components are laid out and the application's appearance. Abra index.css y agréguele el código siguiente.Open index.css and add the following code to it. El estilo @media define las opciones de estilo alternativas que se usarán cuando el ancho de pantalla sea inferior a 700 píxeles.The @media style 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 less 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);
     }
 }

Si ejecuta la aplicación ahora, verá el encabezado, el cuadro de búsqueda y el botón de búsqueda, pero el mapa no será visible porque no se ha cargado aún.If you run the application now, you see the header, search box, and search button, but the map isn't visible because it hasn’t been loaded yet. Si intenta realizar una búsqueda, no ocurre nada.If you try to do a search, nothing happens. Se deberá configurar la lógica de JavaScript que se describe en la sección siguiente para acceder a toda la funcionalidad del localizador de almacén.We need to set up the JavaScript logic that's described in the next section to access all the functionality of the store locator.

Conexión de la aplicación con JavaScriptWire the application with JavaScript

Llegados a este punto, todo está configurado en la interfaz de usuario.At this point, everything is set up in the user interface. Ahora, es necesario agregar el código de JavaScript para cargar y analizar los datos y, luego, representar los datos en el mapa.Now, we need to add the JavaScript to load and parse the data, and then render the data on the map. Para empezar, abra index.js y agréguele código, como se describe en los pasos siguientes.To get started, open index.js and add code to it, as described in the following steps.

  1. Agregue opciones globales para facilitar la actualización de la configuración.Add global options to make settings easier to update. Además, defina variables para el mapa, una ventana emergente, un origen de datos, una capa de icono, un marcador de HTML que muestre el centro de una zona de búsqueda y una instancia de cliente del servicio de búsqueda de Azure Maps.Also, define variables for the map, a pop-up window, a data source, an icon layer, an HTML marker that displays the center of a search area, and 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;
    
  2. Agregue código a index.js.Add code to index.js. El siguiente código inicializa el mapa, agrega un agente de escucha de eventos que espera hasta que la página termina de cargarse, conecta los eventos para supervisar la carga del mapa y activa el botón de búsqueda y el botón My Location (Mi ubicación).The following code initializes the map, adds an event listener that waits until the page is finished loading, wires up events to monitor the loading of the map, and powers the search button and My Location button.

    Cuando el usuario selecciona el botón de búsqueda, o cuando el usuario presiona Entrar después de escribir una ubicación en el cuadro de búsqueda, se inicia una búsqueda aproximada con la consulta del usuario.When the user selects the search button, or when the user presses Enter after entering a location in the search box, a fuzzy search against the user's query is initiated. Pase una matriz de valores de país ISO 2 a la opción countrySet para limitar los resultados de búsqueda a esos países.Pass in an array of country ISO 2 values to the countrySet option to limit the search results to those countries. Limitar los países de búsqueda ayuda a aumentar la precisión de los resultados que se devuelven.Limiting the countries to search helps increase the accuracy of the results that are returned.

    Cuando haya finalizado la búsqueda, tome el primer resultado y establezca la cámara del mapa sobre esa zona.When the search is finished, take the first result and set the map camera over that area. Cuando el usuario seleccione el botón My Location (Mi ubicación), use la API de geolocalización HTML5 que está integrada en el explorador para recuperar la ubicación del usuario y centrar el mapa sobre esta.When the user selects the My Location button, use the HTML5 Geolocation API that's built into the browser to retrieve the user's location and center the map over their location.

    Sugerencia

    Cuando se usen ventanas emergentes, es mejor crear una única instancia de Popup y reutilizarla mediante la actualización de su contenido y posición.When you use pop-up windows, it's best to create a single Popup instance and reuse the instance by updating its content and position. Para cada instancia de Popup que agrega al código, se agregan varios elementos DOM a la página.For every Popupinstance you add to your code, multiple DOM elements are added to the page. Cuantos más elementos DOM haya en una página, de más cosas tiene que realizar el explorador un seguimiento.The more DOM elements there are on a page, the more things the browser has to keep track of. Si hay demasiados elementos, el explorador podría ralentizarse.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 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 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 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 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 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;
    
  3. En el agente de escucha de eventos ready del mapa, agregue un control de zoom y un marcador de HTML para mostrar el centro de una zona de búsqueda.In the map's ready event 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);
    
  4. En el agente de escucha de eventos ready del mapa, agregue un origen de datos.In the map's ready event listener, add a data source. A continuación, realice una llamada para cargar y analizar el conjunto de datos.Then, make a call to load and parse the dataset. Habilite la agrupación en clústeres en el origen de datos.Enable clustering on the data source. La agrupación en clústeres en el origen de datos agrupa los puntos superpuestos en un clúster.Clustering on the data source groups overlapping points together in a cluster. El clúster se separa en puntos individuales cuando el usuario acerca el mapa.The clusters separate into individual points as the user zooms in. Como resultado, la experiencia del usuario es más fluida y se mejora el rendimiento.This makes a more fluid 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();
    
  5. Después de cargar el conjunto de datos en el agente de escucha de eventos ready del mapa, defina un conjunto de capas para representar los datos.After you load the dataset in the map's ready event listener, define a set of layers to render the data. Se usa una capa de burbuja para representar los puntos de datos en clúster.A bubble layer is used to render clustered data points. Se usa una capa de símbolo para representar el número de puntos en cada clúster por encima de la capa de burbuja.A symbol layer is used to render the number of points in each cluster above the bubble layer. Una segunda capa de símbolo representa un icono personalizado para ubicaciones individuales en el mapa.A second symbol layer renders a custom icon for individual locations on the map.

    Agregue los eventos mouseover y mouseout a las capas de burbuja e icono para que el cursor del mouse cambie cuando el usuario lo mantenga sobre un clúster o un icono en el mapa.Add mouseover and mouseout events to the bubble and icon layers to change the mouse cursor when the user hovers over a cluster or icon on the map. Agregue un evento click a la capa de burbuja del clúster.Add a click event to the cluster bubble layer. Este evento click acerca el mapa dos niveles y lo centra sobre un clúster cuando el usuario selecciona cualquiera de ellos.This click event zooms the map in two levels and centers the map over a cluster when the user selects any cluster. Agregue un evento click a la capa de icono.Add a click event to the icon layer. Este evento click muestra una ventana emergente con los detalles de una cafetería cuando un usuario selecciona un icono de ubicación.This click event displays a pop-up window that shows the details of a coffee shop when a user selects an individual location icon. Agregue un evento al mapa para supervisar el momento en que el mapa termina de moverse.Add an event to the map to monitor when the map is finished moving. Cuando se active este evento, actualice los elementos del panel de lista.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: '{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();
    });
    
  6. Cuando se cargue el conjunto de datos de cafeterías, primero debe descargarse.When the coffee shop dataset is loaded, it must first be downloaded. Luego, se debe dividir el archivo de texto en líneas.Then, the text file must be split into lines. La primera línea contiene la información de encabezado.The first line contains the header information. Para que el código sea más fácil de seguir, se analiza el encabezado en un objeto, que luego se puede usar para consultar el índice de celda de cada propiedad.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. Después de la primera línea, recorra en bucle el resto de líneas y cree una característica de punto.After the first line, loop through the remaining lines and create a point feature. Agregue la característica de punto al origen de datos.Add the point feature to the data source. Por último, actualice el panel de lista.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();
        });
    }
    
  7. Cuando se actualiza el panel de lista, se calcula la distancia desde el centro del mapa hasta todas las características de punto de la vista actual del mapa.When the list panel is updated, the distance from the center of the map to all point features in the current map view is calculated. Luego, las características se ordenan por distancia.The features are then sorted by distance. Se genera código HTML para mostrar cada ubicación en el panel de lista.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');
    
        //Get all the shapes that have been rendered in the bubble layer.
        var data = map.layers.getRenderedShapes(map.getCamera().bounds, [iconLayer]);
    
        data.forEach(function(shape) {
            if (shape instanceof atlas.Shape) {
                //Calculate the distance from the center of the map to each shape, and then store the data in a distance property.  
                shape.distance = atlas.math.getDistanceTo(camera.center, shape.getCoordinates(), 'miles');
            }
        });
    
        //Sort the data by distance.
        data.sort(function(x, y) {
            return x.distance - y.distance;
        });
    
        //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>
            */
    
            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 />',
    
                //Route the distance to two decimal places.  
                (Math.round(shape.distance * 100) / 100),
                ' 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('');
    }
    
  8. Cuando el usuario selecciona un elemento en el panel de lista, la forma con la que está relacionado el elemento se recupera del origen de datos.When the user selects an item in the list panel, the shape to which the item is related is retrieved from the data source. Se genera una ventana emergente que se basa en la información de propiedad almacenada en la forma.A pop-up window is generated that's based on the property information stored in the shape. El mapa se centra sobre la forma.The map is centered over the shape. Si el ancho del mapa es inferior a 700 píxeles, la vista del mapa se desplaza para que la ventana emergente sea visible.If the map is less 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 less 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>
        */
    
        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),
    
            //Route the distance to two decimal places.  
            '<br/>', (Math.round(shape.distance * 100) / 100),
            ' 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);
    }
    

Ahora, tiene un localizador de almacén totalmente funcional.Now, you have a fully functional store locator. En un explorador web, abra el archivo index.html correspondiente al localizador de almacén.In a web browser, open the index.html file for the store locator. Cuando los clústeres aparezcan en el mapa, puede buscar una ubicación, bien mediante el cuadro de búsqueda, o también puede seleccionar el botón My Location (Mi ubicación), un clúster o acercar el mapa para ver las ubicaciones individuales.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.

La primera vez que un usuario selecciona el botón My Location (Mi ubicación), el explorador muestra una advertencia de seguridad que solicita permiso para acceder a la ubicación del usuario.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. Si el usuario acepta compartir su ubicación, el mapa la acerca y se muestran las cafeterías cercanas.If the user agrees to share their location, the map zooms in on the user's location, and nearby coffee shops are shown.


Captura de pantalla de la solicitud del explorador para acceder a la ubicación del usuario

Al acercar lo suficiente una zona que tiene ubicaciones de cafetería, los clústeres se separan en ubicaciones individuales.When you zoom in close enough in an area that has coffee shop locations, the clusters separate into individual locations. Seleccione uno de los iconos del mapa o un elemento del panel lateral para ver una ventana emergente que muestre información de esa ubicación.Select one of the icons on the map or select an item in the side panel to see a pop-up window that shows information for that location.


Captura de pantalla del localizador de almacén finalizado

Si cambia de tamaño la ventana del explorador a un ancho inferior a 700 píxeles o abre la aplicación en un dispositivo móvil, el diseño cambia para adaptarse mejor a pantallas más pequeñas.If you resize the browser window to less than 700 pixels wide or open the application on a mobile device, the layout changes to be better suited for smaller screens.


Captura de pantalla de la versión de pantalla pequeña del localizador de almacén

Pasos siguientesNext steps

En este tutorial, aprenderá a crear un localizador de almacén básico mediante Azure Maps.In this tutorial, you learn how to create a basic store locator by using Azure Maps. El localizador de almacén que crea en este tutorial podría tener toda la funcionalidad necesaria.The store locator you create in this tutorial might have all the functionality you need. Puede agregar características a su localizador de almacén o usar características más avanzadas para conseguir una experiencia de usuario más personalizada:You can add features to your store locator or use more advance features for a more custom user experience:

Puede acceder al ejemplo de código de este tutorial, aquí:You can access the code sample for this tutorial here:

Creación de un localizador de almacén mediante Azure MapsCreate a store locator by using Azure Maps

Consulte este ejemplo aquíSee the sample live here

Para más información sobre la cobertura y las funcionalidades de Azure Maps:To learn more about the coverage and capabilities of Azure Maps:

Para ver más ejemplos de código y una experiencia interactiva de codificación:To see more code examples and an interactive coding experience: