Tutorial: usar o Azure Mapas para criar um localizador de lojas

Este tutorial orienta você pelo processo de criação de um localizador de lojas simples usando o Azure Mapas.

Neste tutorial, você aprenderá como:

  • Criar uma nova página da Web usando a API de Controle de Mapeamento do Azure.
  • Carregar dados personalizados de um arquivo e exibi-los em um mapa.
  • Usar o serviço de pesquisa do Azure Mapas para encontrar um endereço ou digitar uma consulta.
  • Obter a localização do usuário do navegador e mostrá-la no mapa.
  • Combinar várias camadas para criar símbolos personalizados no mapa.
  • Agrupar pontos de dados em cluster.
  • Adicionar controles de zoom ao mapa.

Pré-requisitos

Observação

Para obter mais informações sobre a autenticação nos Azure Mapas, confira Gerenciar a autenticação nos Azure Mapas.

Código de exemplo

Este tutorial demonstra como criar um localizador de lojas para uma empresa fictícia chamada Café Contoso, juntamente com dicas para estender o localizador da loja com funcionalidade adicional.

Para ver uma amostra ao vivo do que você criará neste tutorial, consulte Localizador Simples de Lojas no site de Amostras de Código do Azure Mapas.

Para acompanhar e participar mais facilmente deste tutorial, você precisará baixar os seguintes recursos:

Recursos do localizador de lojas

Esta seção lista os recursos do Azure Mapas que são demonstrados no aplicativo de localizador de loja de café da Contoso criado neste tutorial.

Recursos da interface do usuário

  • Um logotipo da loja no cabeçalho
  • Um mapa que dá suporte ao panorâmico e ao zoom
  • Um botão Minha Localização para pesquisar a localização atual do usuário.
  • Um layout da Página se ajusta com base na largura da tela dos dispositivos
  • Uma caixa de pesquisa e um botão de pesquisa

Recursos de funcionalidade

  • Um evento keypress adicionado à caixa de pesquisa disparará uma pesquisa se o usuário pressionar Enter.
  • Quando o mapa é movido, a distância a cada lugar contada a partir do centro do mapa é recalculada. A lista de resultados é atualizada para exibir os lugares mais próximos na parte superior do mapa.
  • Quando o usuário seleciona um resultado na lista de resultados, o mapa é centralizado no local selecionado e as informações sobre o local aparecem em uma janela pop-up.
  • Quando o usuário seleciona uma localização específica, o mapa abre uma janela pop-up.
  • Quando o usuário reduz o mapa, os locais são agrupados em clusters. Cada cluster é representado por um círculo com um número dentro do círculo. Os clusters são formados e separados dependendo do nível de zoom escolhido pelo usuário.
  • A escolha de um cluster amplia o mapa em dois níveis e centraliza sobre a localização do cluster.

Design do localizador de lojas

A captura de tela a seguir mostra o layout geral do aplicativo de localizador de loja da Contoso Coffee. Para exibir e interagir com a amostra ao vivo, consulte o aplicativo de exemplo do Localizador de Lojas Simples no site de Exemplos de Código do Azure Mapas.

A screenshot showing the Contoso Coffee store locator Azure Maps sample application.

Para maximizar a utilidade do localizador de lojas, incluímos um layout dinâmico que se ajusta à largura da tela do usuário quando ela tem menos de 700 pixels de largura. Um layout dinâmico facilita o uso do localizador de lojas em uma tela pequena, como em um dispositivo móvel. Aqui está uma captura de tela mostrando um exemplo do layout de telas pequenas:

A screenshot showing what the Contoso Coffee store locator application looks like on a mobile device.

Criar o conjunto de dados do lugar da loja

Esta seção descreve como criar um conjunto de dados das lojas que você deseja exibir no mapa. O conjunto de dados para o localizador da Contoso Coffee é criado dentro de uma pasta de trabalho do Excel. O conjunto de dados contém 10.213 lojas da Contoso Coffee espalhadas em nove países ou regiões: Alemanha, Canadá, Dinamarca, Espanha, Estados Unidos, França, Itália, Países Baixos e Reino Unido. Aqui está uma captura de tela mostrando como devem ser os dados:

Screenshot of the store locator data in an Excel workbook.

Baixe o arquivo do Excel que contém o conjunto de dados completo para o aplicativo de exemplo do localizador da Café Contoso pode da pasta dados do repositório de exemplos de código do Azure Mapas no GitHub.

A partir da captura de tela dos dados, podemos fazer as seguintes observações:

  • As informações de localização são armazenadas nas seis colunas a seguir: AddressLine, Cidade, Município (região), AdminDivision (estado/província), PostCode (CEP) e País.
  • As colunas Latitude e Longitude contêm as coordenadas de cada localização da Contoso Coffee. Se você não tiver informações sobre as coordenadas, você pode usar o serviço Pesquisa para determinar as coordenadas de localização.
  • Algumas outras colunas contêm metadados relacionados às cafeterias: um número de telefone, colunas de boolianos e horários de abertura e fechamento no formato de 24 horas. As colunas de boolianos referem-se à acessibilidade a cadeira de rodas e Wi-Fi. Você pode criar suas próprias colunas com os metadados mais relevantes para seus dados de localização.

Observação

O Azure Mapas renderiza dados na projeção Mercator esférica "EPSG:3857", mas lê os dados em "EPSG:4326", que usa a datum WGS84.

Carregar conjunto de os conjuntos de o localizador do contoso café

O conjunto de dados do localizador da Contoso Café é pequeno, portanto, ele pode ser convertido em um arquivo de texto delimitado por tabulação que o navegador baixa quando o aplicativo é carregado.

Dica

Se o seu conjunto de dados for muito grande para download do cliente ou for atualizado com frequência, você poderá considerar o armazenamento do seu conjunto de dados em um banco de dados. Depois que os dados forem carregados em um banco de dados, você poderá configurar um serviço Web que aceita consultas para os dados e enviar os resultados para o navegador do usuário.

Converter dados em um arquivo de texto delimitado por tabulação

Para converter os dados de localização da loja da Contoso Coffee de uma planilha do Excel em um arquivo de texto delimitado por tabulação:

  1. Baixe a pasta de trabalho Excel ContosoCoffee.xlsx e abra-a no Excel.

  2. Selecione Arquivo> Salvar como....

  3. Na lista suspensa Salvar como tipo, selecione Texto (delimitado por tabulação)(*.txt) .

  4. Nomeie o arquivo como ContosoCoffee.

Screenshot of the Save as type dialog box.

Se você abrir o arquivo de texto no Bloco de Notas, o texto será mais ou menos assim:

Screenshot of a Notepad file that shows a tab-delimited dataset.

Configurar o projeto

  1. Abra Visual Studio Codeou sua escolha de ambientes de desenvolvimento.

  2. Selecione Arquivo > Abrir Espaço de Trabalho....

  3. Crie uma nova pasta chamada ContosoCoffee.

  4. Selecione ContosoCoffee no explorador.

  5. Crie os três arquivos a seguir que definem o layout, o estilo e a lógica do aplicativo:

    • index.html
    • index.css
    • index.js
  6. Crie uma pasta chamada data.

  7. Adicione o arquivo de ContosoCoffee.txt que você criou anteriormente da ContosoCoffee.xlsx da pasta de trabalho do Excel à pasta de dados.

  8. Crie outra pasta chamada images.

  9. Se você ainda não fez isso, baixe as 10 imagens do mapa do diretório de imagens no repositório GitHub e adicione-as à pasta imagens.

    Agora, sua pasta do espaço de trabalho deve se parecer com a captura de tela abaixo:

    Screenshot of the images folder in the Contoso Coffee directory.

Crie o HTML

Para criar o HTML:

  1. Adicione as seguintes marcas meta ao head de 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">
    
  2. Adicione referências para os arquivos CSS e JavaScript do controle Web do Azure Mapas:

    <!-- 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"></script>
    
  3. Em seguida, adicione uma referência para o módulo dos serviços do Azure Mapas. Este módulo é uma biblioteca JavaScript que encapsula os serviços REST do Azure Mapas, tornando-os fáceis de usar em JavaScript. O módulo Services é útil para viabilizar a funcionalidade de pesquisa.

    <!-- Add a reference to the Azure Maps Services Module JavaScript file. -->
    <script src="https://atlas.microsoft.com/sdk/javascript/service/2/atlas-service.min.js"></script>
    
  4. Adicione referências a index.js e index.css.

    <!-- Add references to the store locator JavaScript and CSS files. -->
    <link rel="stylesheet" href="index.css" type="text/css">
    <script src="index.js"></script>
    
  5. No corpo do documento, adicione uma marca header. Na marca header, adicione o nome do logotipo e da empresa.

    <header>
        <img src="images/Logo.png" />
        <span>Contoso Coffee</span>
    </header>
    
  6. Adicione uma marca main e crie um painel de pesquisa que tenha um botão de pesquisa e uma caixa de texto. Além disso, adicione referências div para o mapa, o painel de lista e o botão de GPS Minha Localização.

    <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>
    

Depois de concluir, o index.html deverá ser parecido com Simple Store Locator.html no código de exemplo do tutorial.

Definir os estilos da CSS

A próxima etapa é definir os estilos da CSS. Os estilos da CSS definem como os componentes do aplicativo são dispostos e a aparência do aplicativo.

  1. Abra index.css.

  2. Adicione o código css seguinte:

    Observação

    O estilo @media define opções de estilo alternativas para uso quando a largura da tela é menor que 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);
         }
     }
    

Se você executar o aplicativo neste ponto, o cabeçalho, a caixa de pesquisa e o botão de pesquisa serão exibidos. Porém, o mapa não está visível porque ainda não foi carregado. Se você tentar fazer uma pesquisa, nada acontecerá. A próxima seção descreve a adição da lógica JavaScript necessária para acessar todas as funcionalidades do localizador do repositório.

Adicionar o código JavaScript

O código JavaScript no aplicativo do localizador da loja da Contoso Coffee habilita os seguintes processos:

  1. Adiciona um ouvinte de eventos chamado de ready para aguardar até que a página tenha concluído o processo de carregamento. Quando o carregamento da página for concluído, o manipulador de eventos criará mais ouvintes de eventos para monitorar o carregamento do mapa e dará funcionalidade aos botões Pesquisar e Minha localização.

  2. Quando o usuário seleciona o botão de pesquisa ou digita uma localização na caixa de pesquisa e seleciona ENTER, uma pesquisa difusa é iniciada na consulta do usuário começa. O código passa em uma matriz de valores de país/região ISO 2 para a opção countrySet a fim de limitar os resultados da pesquisa a esses países/regiões. Limitar os países/regiões a serem pesquisados ajuda a aumentar a precisão dos resultados retornados.

  3. Depois que a pesquisa for concluída, o primeiro resultado da localização será usado como o foco central do mapa. Quando o usuário seleciona o botão Minha Localização, o código recupera a localização do usuário usando a API de Geolocalização do HTML5 interna do navegador. Depois de recuperar a localização, o código centralizará o mapa sobre a localização do usuário.

Para adicionar o JavaScript:

  1. Abra index.js.

  2. Adicione opções globais para facilitar a atualização das configurações. Defina as variáveis para o mapa, a janela pop-up, a fonte de dados, a camada de ícone e o marcador HTML. Defina o marcador HTML para indicar o centro de uma área de pesquisa. Defina também uma instância do cliente do serviço de pesquisa dos Azure Mapas.

    //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';
    
    //An array of country region ISO2 values to limit searches to.
    var countrySet = ['US', 'CA', 'GB', 'FR','DE','IT','ES','NL','DK'];      
    
    //
    var map, popup, datasource, iconLayer, centerMarker, searchURL;
    
    // Used in function updateListItems
    var listItemTemplate = '<div class="listItem" onclick="itemSelected(\'{id}\')"><div class="listItem-title">{title}</div>{city}<br />Open until {closes}<br />{distance} miles away</div>';
    
    
  3. Adicione o seguinte código de inicialização. Certifique-se de substituir <Your Azure Maps Key> pela sua chave de assinatura do Azure Mapas.

    Dica

    Quando você usa janelas pop-up, é melhor criar uma instância de Popup única e reutilizá-la atualizando seu conteúdo e posição. Para cada instância de Popup que você adicionar ao seu código, vários elementos de DOM serão adicionados à página. Quanto mais elementos de DOM houver em uma página, mais itens o navegador terá que controlar. Se houver muitos itens, o navegador poderá ficar lento.

    
    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 MapControlCredential to share authentication between a map control and the service module.
        var pipeline = atlas.service.MapsURL.newPipeline(new atlas.service.MapControlCredential(map));
    
        //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 maps post load functionality.
    
        });
    }
    
    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,
            view: 'Auto'
        }).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 into a longitude/latitude position value the map can understand 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 trying to access the users 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;
    
  4. No manipulador de evento ready do mapa, adicione um controle de zoom e um marcador de HTML para exibir o centro de uma área de pesquisa.

    //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);
    
  5. No manipulador de evento ready do mapa, adicione uma fonte de dados. Em seguida, faça uma chamada de carregamento e analise o conjunto de dados. Habilite o clustering na fonte de dados. Clustering dos pontos sobrepostos dos grupos da fonte de dados em um cluster. Quando o usuário amplia o mapa, os clusters se dividem em pontos individuais. Esse comportamento fornece uma experiência do usuário melhor e aprimora o desempenho.

    //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 has been defined.  
    loadStoreData();
    
  6. Depois de carregado o conjunto de dados no manipulador de evento ready do mapa, defina um conjunto de camadas para renderizar os dados. Uma camada de bolha renderiza pontos de dados clusterizados. Uma camada de símbolo renderiza o número de pontos em cada cluster acima da camada de bolha. Uma segunda camada de símbolo renderiza um ícone personalizado de lugares individuais no mapa.

    Adicione os eventos mouseover e mouseout às camadas de bolha e de ícone para alterar o cursor do mouse quando o usuário passa o mouse sobre um ícone ou cluster no mapa. Adicione um evento click à camada de bolha do cluster. Este evento click amplia o mapa em dois níveis e centraliza o mapa sobre um cluster quando o usuário seleciona um cluster. Adicione um evento click à camada de ícone. Esse evento click exibe uma janela pop-up que mostra os detalhes de uma cafeteria quando um usuário seleciona um ícone de localização individual. Adicione um evento ao mapa para monitorar quando o mapa para de se movimentar. Quando esse evento é acionado, atualize os itens no painel de listas.

    //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 has finished rendering.
       map.events.add('render', function() {
           //Update the data in the list.
           updateListItems();
       });
    });
    
  7. Quando o conjunto de dados da cafeteria é necessário, ele deve ser baixado primeiro. Depois de baixado, o arquivo deve ser dividido em linhas. A primeira linha contém as informações de cabeçalho. Para facilitar o entendimento do código a seguir, vamos analisar o cabeçalho em um objeto, que podemos usar para pesquisar o índice de célula de cada propriedade. Após a primeira linha, percorra as linhas restantes e crie um recurso de ponto. Adicione o recurso de ponto à fonte de dados. Por fim, atualize o painel de lista.

    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();
        });
    }
    
  8. Quando o painel de lista é atualizado, a distância é calculada. Essa distância é contada do centro do mapa a todos os recursos de ponto na exibição de mapa atual. Os recursos, em seguida, são classificados pela distância. HTML é gerado para exibir cada local no painel de lista.

    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 if the user is zoomed out a substantial distance. If they are, tell them 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('');
    }
    
  9. Quando o usuário seleciona um item no painel de lista, a forma à qual o item está relacionado é recuperada da fonte de dados. Uma janela pop-up é gerada com base nas informações de propriedade armazenadas na forma. O mapa é centralizado sobre a forma. Se o mapa tiver menos de 700 pixels de largura, a exibição do mapa será deslocada para que a janela pop-up fique visível.

    //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);
    }
    

Agora você tem um localizador de lojas totalmente funcional. Abra o arquivo index.html em seu navegador da Web. Quando os clusters aparecem no mapa, você pode pesquisar um local usando qualquer um dos seguintes métodos:

  1. A caixa de pesquisa.
  2. Selecionando o botão Minha Localização
  3. Selecionando um cluster
  4. Ampliar o mapa para ver locais individuais.

Na primeira vez em que um usuário seleciona o botão Minha Localização, o navegador exibe um aviso de segurança que solicita permissão para acessar a localização dele. Se o usuário concordar em compartilhar sua localização, o mapa ampliará a localização do usuário e as cafeterias próximas serão exibidas.

Screenshot of the browser's request to access the user's location

Quando você ampliar o suficiente em uma área com cafeterias, os clusters se dividem em lugares individuais. Selecione um dos ícones no mapa ou um item no painel lateral para ver uma janela pop-up. O pop-up mostra informações para a localização selecionada.

Screenshot of the finished store locator.

Se você redimensionar a janela do navegador para menos de 700 pixels de largura ou abrir o aplicativo em um dispositivo móvel, o layout muda para se adequar melhor a telas menores.

Screenshot of the small-screen version of the store locator

Neste tutorial, você aprendeu a criar um localizador de lojas básico usando o Azure Mapas. O localizador de lojas que você criou neste tutorial pode ter todas as funcionalidades desejadas. Você pode adicionar recursos ao seu localizador de lojas ou usar recursos mais avançados para uma experiência do usuário mais personalizada:

Informações adicionais

Próximas etapas

Para ver mais exemplos de código e uma experiência interativa de codificação: