Skip to content

Store Locator using REST API Queries as Data Source

This guide shows you how to build a store locator using REST API queries that return responses containing the store locations data in JSON format. The store locations are processed to GeoJSON format before being added to the map using the geojson.js library.

The finished example will look like this:

See the example fullscreen

Store locations are loaded dynamically from the server only for the region of the map which is visible.

When to use

  • The store location database is large, deployed on a server, and only a small part is downloaded to the client on demand basis.

  • The data set is changing frequently.

What is needed

  • Magic Lane API key token

  • Web server (an example is provided)

  • REST API for a database containing the store locations

Setup

Get your Magic Lane API key token: if you do not have a token, see the Getting Started guide.

This project needs a web server. If you do not have access to a web server, you can easily install a local web server, see the Installing a Local Web Server guide. In this project we use a local web server.

Adding stores data

 1// Start by setting your token from https://developer.magiclane.com/api/projects
 2if (gem.core.App.token === undefined) gem.core.App.token = '';
 3
 4const defaultAppScreen = gem.core.App.initAppScreen({
 5     container: 'map-canvas',
 6     center: [48.8562, 2.3516, 10000] // Paris
 7});
 8
 9const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
10     markerBubble: {
11             title: ['name'],
12             image: ['preview']
13     },
14     markerGrouping: {
15             maxLevel: 14,
16             style: gem.control.MarkersGroupStyleType.default
17     }
18}, {
19     markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
20             const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
21                     'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + '&limit=1000&langs=all';
22
23             return fetch(dataFetchUrl, { method: 'GET', signal: signal })
24                     .then(response => response.json())
25                     .then(responseJson => {
26                             return jsonToGeoJSON(responseJson);
27                     })
28                     .catch((e) => {
29                             return '';
30                     });
31     }
32});
33defaultAppScreen.addControl(dataQueryControl);
34
35function jsonToGeoJSON(jsonData) {
36     jsonData.forEach(element => {
37             const propertiesJson = JSON.parse(element.properties);
38             for (const [key, value] of Object.entries(propertiesJson.properties)) {
39                     element[key] = value;
40             }
41             delete element.properties;
42     });
43     const GeoJSONdata = GeoJSON.parse(jsonData, { Point: ['st_y', 'st_x'] });
44     return GeoJSONdata;
45}
The map control is initialized using gem.core.App.initAppScreen. The map is rendered in the div container with the id map-canvas. The map is centered on Paris, France using coordinate latitude, longitude and altitude.

The control class gem.control.CustomQueryAddedDataControl is used to add data loaded dynamically from the server through the option markersFeedFunction. Before being added to the map the JSON response is processed into GeoJSON data using the function jsonToGeoJSON().
 1 <!DOCTYPE html>
 2 <html lang="en-us">
 3   <head>
 4     <meta charset="utf-8" />
 5     <title>Store Locator REST API data source - MagicLane Maps SDK for JavaScript</title>
 6     <link rel="stylesheet" type="text/css" href="https://www.magiclane.com/sdk/js/gem.css" />
 7     <link rel="stylesheet" href="/fonts/webfonts.css" type="text/css" media="all" />
 8   </head>
 9
10   <body>
11     <div id="store-locator" style="width: 100%; height: 100%">
12       <div id="map-canvas" style="width: 100%; height: 100%; position: absolute; overflow: hidden"></div>
13     </div>
14
15     <script src="https://www.magiclane.com/sdk/js/gemapi.js"></script>
16     <script type="text/javascript" src="token.js"></script>
17     <script type="text/javascript" src="geojson.min.js"></script>
18     <script type="text/javascript" src="storeRESTQueryRequests01.js"></script>
19   </body>
20 </html>

Adding stores list

 1 const listUIControl = new gem.control.ListControl({
 2     sourceControl: dataQueryControl,
 3     container: 'stores-list',
 4     displayCount: true,
 5     flyToItemAltitude: 300,
 6     menuName: 'REST API data source',
 7     titleProperties: ['name'],
 8     detailsProperties: ['language'],
 9     imageProperty: ['preview']
10 });
11 defaultAppScreen.addControl(listUIControl);
The list element is initialized using the control class gem.control.ListControl. The list is connected to the data source control using option sourceControl and is appended to the parent element div using the option container.

The list is customized using the option populateItemFunction and custom style rules are defined in the <style> section of the HTML file for brevity of the example.
1...
2<body>
3  <div id="store-locator" style="width: 100%; height: 100%">
4    <div id="stores-list" class="menu-list-container" style="width: 25%; height: 100%; position: absolute"></div>
5    <div id="map-canvas" style="width: 75%; left: 25%; height: 100%; position: absolute; overflow: hidden"></div>
6  </div>
7...

Adding filters control

To add filters for your data you can follow the boilerplate code below:

 1 const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
 2     markerBubble: {
 3             title: ['name'],
 4             image: ['preview']
 5     },
 6     markerGrouping: {
 7             maxLevel: 14,
 8             style: gem.control.MarkersGroupStyleType.default
 9     }
10 }, {
11     markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
12             const strUrlParams = filtersToUrlParams(filters);
13
14             const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
15                     'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + strUrlParams;
16
17             return fetch(dataFetchUrl, { method: 'GET', signal: signal })
18                     .then(response => response.json())
19                     .then(responseJson => {
20                             return jsonToGeoJSON(responseJson);
21                     })
22                     .catch((e) => {
23                             return '';
24                     });
25     }
26 });
27 defaultAppScreen.addControl(dataQueryControl);
28
29 const radioLimitFilter = new gem.control.CategoryFilterControl({
30     parentContainer: 'filter-category-limit',
31     name: 'byLimit',
32     title: 'Limit:',
33     categories: [
34             { label: '100 items', filter: { key: 'limit', value: '100' } },
35             { label: '500 items', filter: { key: 'limit', value: '500' } },
36             { label: '1000 items', filter: { key: 'limit', value: '1000' } }
37     ]
38 }, dataQueryControl);
39
40 const checkboxesLanguageFilterControl = new gem.control.CategoryFilterControl({
41     parentContainer: 'filter-category-language',
42     name: 'byLanguage',
43     style: 'checkboxes',
44     title: 'Filter by Languages',
45     categories: [
46             {
47                     label: 'All', filter: { key: 'langs', value: '' }, children:
48                             [
49                                     { label: 'English', filter: { key: 'langs', value: 'en' } },
50                                     { label: 'Dutch', filter: { key: 'langs', value: 'nl' } },
51                                     { label: 'Finnish', filter: { key: 'langs', value: 'fi' } },
52                                     { label: 'Romanian', filter: { key: 'langs', value: 'ro' } },
53                                     { label: 'Spanish', filter: { key: 'langs', value: 'es' } },
54                                     { label: 'German', filter: { key: 'langs', value: 'de' } },
55                                     { label: 'Hungarian', filter: { key: 'langs', value: 'hu' } }
56                             ]
57             }
58     ]
59 }, dataQueryControl);
60 defaultAppScreen.addControl(checkboxesLanguageFilterControl);
61 defaultAppScreen.addControl(radioLimitFilter);
62
63 function filtersToUrlParams(filters) {
64     let strParams = '';
65     for (const [name, filterValues] of filters.entries()) {
66             if (name === 'byLimit') {
67                     strParams += '&' + filterValues[0].key + '=' + filterValues[0].value;
68             }
69             else if (name === 'byLanguage') {
70                     if (!filterValues.length) {
71                             strParams += '&langs=all';
72                             continue;
73                     }
74                     strParams += '&' + filterValues[0].key + '=';
75                     for (let i = 0; i < filterValues.length; i++) {
76                             strParams += filterValues[i].value + ',';
77                     }
78             }
79     }
80     if (!filters.has('byLimit')) {
81             strParams += '&limit=100';
82     }
83     if (!filters.has('byLanguage')) {
84             strParams += '&langs=all';
85     }
86     const strUrlParams = encodeURI(strParams);
87     return strUrlParams;
88 }
The filter elements are initialized using the control class gem.control.CategoryFilterControl.

The first filter radioLimitFilter is used for limiting the number of results shown on the map and is using the radio style to display the available options.
The second filter checkboxesLanguageFilterControl is used for filtering the data based on the language of the items and is using the checkboxes style to display the options.
The function filtersToUrlParams() is used to transform the received filters map into URL parameters for the REST API of the database. The URL parameters are then used in the data source request with the option markersFeedFunction from the data source control variable dataQueryControl.
 1...
 2<style>
 3  .filters-container {
 4    position: absolute;
 5    width: auto;
 6    max-width: 300px;
 7    right: 0px;
 8    top: 50px;
 9    margin: 8px;
10    z-index: 9;
11    font-size: 0.7rem;
12    background-color: white;
13    box-shadow: 0 2px 4px rgb(0 0 0 / 20%), 0 -1px 0 rgb(0 0 0 / 2%);
14  }
15  ...
16  <div id="filters-container" class="filters-container">
17    <div id="filter-category-language" class="filter-category-language"></div>
18    <div id="filter-category-limit" class="filter-category-limit"></div>
19  </div>
20  ...

Complete example code

  1 // Start by setting your token from https://developer.magiclane.com/api/projects
  2 if (gem.core.App.token === undefined) gem.core.App.token = '';
  3
  4 const defaultAppScreen = gem.core.App.initAppScreen({
  5     container: 'map-canvas',
  6     center: [48.8562, 2.3516, 10000] // Paris
  7 });
  8
  9 const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
 10     markerBubble: {
 11             title: ['name'],
 12             image: ['preview']
 13     },
 14     markerGrouping: {
 15             maxLevel: 14,
 16             style: gem.control.MarkersGroupStyleType.default
 17     }
 18 }, {
 19     markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
 20             const strUrlParams = filtersToUrlParams(filters);
 21
 22             const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
 23                     'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + strUrlParams;
 24
 25             return fetch(dataFetchUrl, { method: 'GET', signal: signal })
 26                     .then(response => response.json())
 27                     .then(responseJson => {
 28                             return jsonToGeoJSON(responseJson);
 29                     })
 30                     .catch((e) => {
 31                             return '';
 32                     });
 33     }
 34 });
 35 defaultAppScreen.addControl(dataQueryControl);
 36
 37 const radioLimitFilter = new gem.control.CategoryFilterControl({
 38     parentContainer: 'filter-category-limit',
 39     name: 'byLimit',
 40     title: 'Filter by Limit',
 41     categories: [
 42             { label: '100 items', filter: { key: 'limit', value: '100' } },
 43             { label: '500 items', filter: { key: 'limit', value: '500' } },
 44             { label: '1000 items', filter: { key: 'limit', value: '1000' } }
 45     ]
 46 }, dataQueryControl);
 47
 48 const checkboxesLanguageFilterControl = new gem.control.CategoryFilterControl({
 49     parentContainer: 'filter-category-language',
 50     name: 'byLanguage',
 51     style: 'checkboxes',
 52     title: 'Filter by Languages',
 53     categories: [
 54             {
 55                     label: 'All', filter: { key: 'langs', value: '' }, children:
 56                             [
 57                                     { label: 'English', filter: { key: 'langs', value: 'en' } },
 58                                     { label: 'Dutch', filter: { key: 'langs', value: 'nl' } },
 59                                     { label: 'Finnish', filter: { key: 'langs', value: 'fi' } },
 60                                     { label: 'Romanian', filter: { key: 'langs', value: 'ro' } },
 61                                     { label: 'Spanish', filter: { key: 'langs', value: 'es' } },
 62                                     { label: 'German', filter: { key: 'langs', value: 'de' } },
 63                                     { label: 'Hungarian', filter: { key: 'langs', value: 'hu' } }
 64                             ]
 65             }
 66     ]
 67 }, dataQueryControl);
 68 defaultAppScreen.addControl(checkboxesLanguageFilterControl);
 69 defaultAppScreen.addControl(radioLimitFilter);
 70
 71
 72 const searchControl = new gem.control.SearchControl({
 73     highlightOptions: {
 74             contourColor: { r: 0, g: 255, b: 0, a: 0 }
 75     },
 76     searchPreferences: {
 77             exactMatch: true,
 78             maximumMatches: 3,
 79             addressSearch: true,
 80             mapPoisSearch: true,
 81             setCursorReferencePoint: true
 82     }
 83 });
 84 defaultAppScreen.addControl(searchControl);
 85
 86 const listUIControl = new gem.control.ListControl({
 87     sourceControl: dataQueryControl,
 88     container: 'stores-list',
 89     displayCount: true,
 90     flyToItemAltitude: 300,
 91     menuName: 'REST API data source',
 92     titleProperties: ['name'],
 93     detailsProperties: ['language'],
 94     imageProperty: ['preview']
 95 });
 96 defaultAppScreen.addControl(listUIControl);
 97
 98 function filtersToUrlParams(filters) {
 99     let strParams = '';
100     for (const [name, filterValues] of filters.entries()) {
101             if (name === 'byLimit') {
102                     strParams += '&' + filterValues[0].key + '=' + filterValues[0].value;
103             }
104             else if (name === 'byLanguage') {
105                     if (!filterValues.length) {
106                             strParams += '&langs=all';
107                             continue;
108                     }
109                     strParams += '&' + filterValues[0].key + '=';
110                     for (let i = 0; i < filterValues.length; i++) {
111                             strParams += filterValues[i].value + ',';
112                     }
113             }
114     }
115     if (!filters.has('byLimit')) {
116             strParams += '&limit=100';
117     }
118     if (!filters.has('byLanguage')) {
119             strParams += '&langs=all';
120     }
121     const strUrlParams = encodeURI(strParams);
122     return strUrlParams;
123 }
124
125 function jsonToGeoJSON(jsonData) {
126     jsonData.forEach(element => {
127             const propertiesJson = JSON.parse(element.properties);
128             for (const [key, value] of Object.entries(propertiesJson.properties)) {
129                     element[key] = value;
130             }
131             delete element.properties;
132     });
133     const GeoJSONdata = GeoJSON.parse(jsonData, { Point: ['st_y', 'st_x'] });
134     return GeoJSONdata;
135 }
 1<!DOCTYPE html>
 2<html lang="en-us">
 3  <head>
 4    <meta charset="utf-8" />
 5    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no, shrink-to-fit=no" />
 6    <title>Store Locator REST API data source - MagicLane Maps SDK for JavaScript</title>
 7    <link rel="stylesheet" type="text/css" href="https://www.magiclane.com/sdk/js/gem.css" />
 8    <link rel="stylesheet" href="/fonts/webfonts.css" type="text/css" media="all" />
 9
10    <style>
11      .filters-container {
12        position: absolute;
13        width: auto;
14        max-width: 300px;
15        right: 0px;
16        top: 50px;
17        margin: 8px;
18        z-index: 9;
19        font-size: 0.7rem;
20        background-color: white;
21        box-shadow: 0 2px 4px rgb(0 0 0 / 20%), 0 -1px 0 rgb(0 0 0 / 2%);
22      }
23    </style>
24  </head>
25
26  <body>
27    <div id="store-locator" style="width: 100%; height: 100%">
28      <div id="stores-list" class="menu-list-container" style="width: 25%; height: 100%; position: absolute"></div>
29      <div id="map-canvas" style="width: 75%; left: 25%; height: 100%; position: absolute; overflow: hidden"></div>
30
31      <div id="filters-container" class="filters-container">
32        <div id="filter-category-language" class="filter-category-language"></div>
33        <div id="filter-category-limit" class="filter-category-limit"></div>
34      </div>
35    </div>
36
37    <script src="https://www.magiclane.com/sdk/js/gemapi.js"></script>
38    <script type="text/javascript" src="token.js"></script>
39    <script type="text/javascript" src="geojson.min.js"></script>
40    <script type="text/javascript" src="storeRESTQueryRequests.js"></script>
41  </body>
42</html>

Files

Source code for this example:

JavaScript
HTML

right-click on the links and select Save As.

For transforming JSON data into GeoJSON we are using geojson.js

JavaScript Examples

Maps SDK for JavaScript Examples can be downloaded or cloned with Git