Skip to content

Store Locator - Switch from Google Maps to Magic Lane Platform

In this guide you will learn how to switch from a simple store locator using Google Maps JavaScript API to using Magic Lane JavaScript API.

Google Maps Simple Store Locator

To create a map which displays store locations from a GeoJSON data source using the Google Places JavaScript API you would write something like the following example code from Google Maps Simple Store Locator:
  1 function initMap() {
  2    // Create the map.
  3     const map = new google.maps.Map(document.getElementById('map'), {
  4         zoom: 7,
  5         center: { lat: 52.632469, lng: -1.689423 },
  6     });
  7
  8     // Load the stores GeoJSON onto the map.
  9     map.data.loadGeoJson('stores.json', {idPropertyName: 'storeid'});
 10
 11     // Define the custom marker icons, using the store's "category".
 12     map.data.setStyle((feature) => {
 13       return {
 14         icon: {
 15           url: `img/icon_${feature.getProperty('category')}.png`,
 16           scaledSize: new google.maps.Size(64, 64),
 17         },
 18       };
 19     });
 20
 21     const apiKey = 'YOUR_API_KEY';
 22     const infoWindow = new google.maps.InfoWindow();
 23
 24     // Show the information for a store when its marker is clicked.
 25     map.data.addListener('click', (event) => {
 26       const category = event.feature.getProperty('category');
 27       const name = event.feature.getProperty('name');
 28       const description = event.feature.getProperty('description');
 29       const hours = event.feature.getProperty('hours');
 30       const phone = event.feature.getProperty('phone');
 31       const position = event.feature.getGeometry().get();
 32       const content = `
 33         <h2>${name}</h2><p>${description}</p>
 34         <p><b>Open:</b> ${hours}<br/><b>Phone:</b> ${phone}</p>
 35       `;
 36
 37       infoWindow.setContent(content);
 38       infoWindow.setPosition(position);
 39       infoWindow.setOptions({pixelOffset: new google.maps.Size(0, -30)});
 40       infoWindow.open(map);
 41     });
 42
 43     // Build and add the search bar
 44     const card = document.createElement('div');
 45     const titleBar = document.createElement('div');
 46     const title = document.createElement('div');
 47     const container = document.createElement('div');
 48     const input = document.createElement('input');
 49     const options = {
 50       types: ['address'],
 51       componentRestrictions: {country: 'gb'},
 52     };
 53
 54     card.setAttribute('id', 'pac-card');
 55     title.setAttribute('id', 'title');
 56     title.textContent = 'Find the nearest store';
 57     titleBar.appendChild(title);
 58     container.setAttribute('id', 'pac-container');
 59     input.setAttribute('id', 'pac-input');
 60     input.setAttribute('type', 'text');
 61     input.setAttribute('placeholder', 'Enter an address');
 62     container.appendChild(input);
 63     card.appendChild(titleBar);
 64     card.appendChild(container);
 65     map.controls[google.maps.ControlPosition.TOP_RIGHT].push(card);
 66
 67     // Make the search bar into a Places Autocomplete search bar and select
 68     // which detail fields should be returned about the place that
 69     // the user selects from the suggestions.
 70     const autocomplete = new google.maps.places.Autocomplete(input, options);
 71
 72     autocomplete.setFields(
 73         ['address_components', 'geometry', 'name']);
 74
 75     // Set the origin point when the user selects an address
 76     const originMarker = new google.maps.Marker({map: map});
 77     originMarker.setVisible(false);
 78     let originLocation = map.getCenter();
 79
 80     autocomplete.addListener('place_changed', async () => {
 81       originMarker.setVisible(false);
 82       originLocation = map.getCenter();
 83       const place = autocomplete.getPlace();
 84
 85       if (!place.geometry) {
 86         // User entered the name of a Place that was not suggested and
 87         // pressed the Enter key, or the Place Details request failed.
 88         window.alert('No address available for input: \'' + place.name + '\'');
 89         return;
 90       }
 91
 92       // Recenter the map to the selected address
 93       originLocation = place.geometry.location;
 94       map.setCenter(originLocation);
 95       map.setZoom(9);
 96       console.log(place);
 97
 98       originMarker.setPosition(originLocation);
 99       originMarker.setVisible(true);
100
101       // Use the selected address as the origin to calculate distances
102       // to each of the store locations
103       const rankedStores = await calculateDistances(map.data, originLocation);
104       showStoresList(map.data, rankedStores);
105
106       return;
107     });
108  }
109
110  async function calculateDistances(data, origin) {
111    const stores = [];
112    const destinations = [];
113
114    // Build parallel arrays for the store IDs and destinations
115    data.forEach((store) => {
116      const storeNum = store.getProperty('storeid');
117      const storeLoc = store.getGeometry().get();
118
119      stores.push(storeNum);
120      destinations.push(storeLoc);
121    });
122
123    // Retrieve the distances of each store from the origin
124    // The returned list will be in the same order as the destinations list
125    const service = new google.maps.DistanceMatrixService();
126    const getDistanceMatrix =
127      (service, parameters) => new Promise((resolve, reject) => {
128        service.getDistanceMatrix(parameters, (response, status) => {
129          if (status != google.maps.DistanceMatrixStatus.OK) {
130            reject(response);
131          } else {
132            const distances = [];
133            const results = response.rows[0].elements;
134            for (let j = 0; j < results.length; j++) {
135              const element = results[j];
136              const distanceText = element.distance.text;
137              const distanceVal = element.distance.value;
138              const distanceObject = {
139                storeid: stores[j],
140                distanceText: distanceText,
141                distanceVal: distanceVal,
142              };
143              distances.push(distanceObject);
144            }
145
146            resolve(distances);
147          }
148        });
149      });
150
151    const distancesList = await getDistanceMatrix(service, {
152      origins: [origin],
153      destinations: destinations,
154      travelMode: 'DRIVING',
155      unitSystem: google.maps.UnitSystem.METRIC,
156    });
157
158    distancesList.sort((first, second) => {
159      return first.distanceVal - second.distanceVal;
160    });
161
162    return distancesList;
163  }
164
165  function showStoresList(data, stores) {
166    if (stores.length == 0) {
167      console.log('empty stores');
168      return;
169    }
170
171    let panel = document.createElement('div');
172    // If the panel already exists, use it. Else, create it and add to the page.
173    if (document.getElementById('panel')) {
174      panel = document.getElementById('panel');
175      // If panel is already open, close it
176      if (panel.classList.contains('open')) {
177        panel.classList.remove('open');
178      }
179    } else {
180      panel.setAttribute('id', 'panel');
181      const body = document.body;
182      body.insertBefore(panel, body.childNodes[0]);
183    }
184
185
186    // Clear the previous details
187    while (panel.lastChild) {
188      panel.removeChild(panel.lastChild);
189    }
190
191    stores.forEach((store) => {
192      // Add store details with text formatting
193      const name = document.createElement('p');
194      name.classList.add('place');
195      const currentStore = data.getFeatureById(store.storeid);
196      name.textContent = currentStore.getProperty('name');
197      panel.appendChild(name);
198      const distanceText = document.createElement('p');
199      distanceText.classList.add('distanceText');
200      distanceText.textContent = store.distanceText;
201      panel.appendChild(distanceText);
202    });
203
204    // Open the panel
205    panel.classList.add('open');
206
207    return;
208  }
  1 <html>
  2
  3 <head>
  4     <title>Google Store Locator</title>
  5     <style>
  6         #map {
  7             height: 100%;
  8         }
  9
 10         html,
 11         body {
 12             height: 100%;
 13             margin: 0;
 14             padding: 0;
 15         }
 16
 17         /* Styling for Autocomplete search bar */
 18         #pac-card {
 19           background-color: #fff;
 20           border-radius: 2px 0 0 2px;
 21           box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
 22           box-sizing: border-box;
 23           font-family: Roboto;
 24           margin: 10px 10px 0 0;
 25           -moz-box-sizing: border-box;
 26           outline: none;
 27         }
 28
 29         #pac-container {
 30           padding-top: 12px;
 31           padding-bottom: 12px;
 32           margin-right: 12px;
 33         }
 34
 35         #pac-input {
 36           background-color: #fff;
 37           font-family: Roboto;
 38           font-size: 15px;
 39           font-weight: 300;
 40           margin-left: 12px;
 41           padding: 0 11px 0 13px;
 42           text-overflow: ellipsis;
 43           width: 400px;
 44         }
 45
 46         #pac-input:focus {
 47           border-color: #4d90fe;
 48         }
 49
 50         #title {
 51           color: #fff;
 52           background-color: #acbcc9;
 53           font-size: 18px;
 54           font-weight: 400;
 55           padding: 6px 12px;
 56         }
 57
 58         .hidden {
 59           display: none;
 60         }
 61
 62         /* Styling for an info pane that slides out from the left.
 63          * Hidden by default. */
 64         #panel {
 65           height: 100%;
 66           width: null;
 67           background-color: white;
 68           position: fixed;
 69           z-index: 1;
 70           overflow-x: hidden;
 71           transition: all .2s ease-out;
 72         }
 73
 74         .open {
 75           width: 250px;
 76         }
 77
 78         .place {
 79           font-family: 'open sans', arial, sans-serif;
 80           font-size: 1.2em;
 81           font-weight: 500;
 82           margin-block-end: 0px;
 83           padding-left: 18px;
 84           padding-right: 18px;
 85         }
 86
 87         .distanceText {
 88           color: silver;
 89           font-family: 'open sans', arial, sans-serif;
 90           font-size: 1em;
 91           font-weight: 400;
 92           margin-block-start: 0.25em;
 93           padding-left: 18px;
 94           padding-right: 18px;
 95         }
 96     </style>
 97 </head>
 98
 99 <body>
100     <!-- The div to hold the map -->
101     <div id="map"></div>
102
103     <script src="app.js"></script>
104     <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap">
105     </script>
106 </body>
107
108 </html>

Magic Lane Store Locator Example

The first step is to set your API key token gem.core.App.token, which you can get at the Magic Lane website, see the Getting Started tutorial.
You only need to type your email address and create a new password.

To create a store locator using a GeoJSON data source you would write something similar to the following example code:

 1 // Start by setting your token from https://developer.magiclane.com/api/projects
 2 if (gem.core.App.token === undefined)
 3     gem.core.App.token = "";
 4
 5 var defaultAppScreen = gem.core.App.initAppScreen({
 6     container: "map-canvas"
 7 });
 8 let geojsonDataControl = new gem.control.GeoJsonAddedDataControl("Oslo_Shops_from_OpenTripMap.geojson", "" /*icon default*/, "" /*no icon filter*/,
 9     {
10             markerBubble: {
11                     title: ['name'],
12                     image: ['preview']
13             }
14     });
15 defaultAppScreen.addControl(geojsonDataControl);
16
17 let listUIControl = new gem.control.ListControl({
18     sourceControl: geojsonDataControl,
19     container: 'menu-list-container',
20     menuName: 'Store Locator Example',
21     titleProperties: ['name'],     // GeoJSON property keys to use for store title
22     detailsProperties: ['kinds'],  // GeoJSON property keys to use for store details
23     imageProperty: ['preview']         // GeoJSON property key to use for store image
24 });
25 defaultAppScreen.addControl(listUIControl);
26
27 let searchControl = new gem.control.SearchControl({
28     highlightOptions: {
29             contourColor: { r: 0, g: 255, b: 0, a: 0 }
30     },
31     searchPreferences: {
32             exactMatch: true,
33             maximumMatches: 3,
34             addressSearch: true,
35             mapPoisSearch: true,
36             setCursorReferencePoint: true
37     }
38 });
39 defaultAppScreen.addControl(searchControl);
 1 <!doctype html>
 2 <html lang="en-us">
 3
 4   <head>
 5     <meta charset="utf-8">
 6     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no, shrink-to-fit=no" />
 7     <title>Store Locator using a GeoJSON data source - Maps SDK for JavaScript</title>
 8     <link rel="stylesheet" type="text/css" href="https://www.magiclane.com/sdk/js/gem.css">
 9   </head>
10
11   <body>
12     <div id="store-locator" style="width: 100%; height: 100%">
13       <div id="menu-list-container" class="menu-list-container"
14         style="width: 30%; height: 100%; left: 70%; position: absolute;"></div>
15       <div id="map-canvas" style="width: 70%; right: 30%; height: 100%; position:absolute; overflow:hidden"></div>
16     </div>
17
18     <script src="https://www.magiclane.com/sdk/js/gemapi.js"></script>
19     <script type="text/javascript" src="token.js"></script>
20     <script type="text/javascript" src="jsStoreGeojsonFile.js"></script>
21
22   </body>
23 </html>
See the example fullscreen