Skip to content

Store Locator with Draw Area Controls using REST API Queries as Data Source

This guide shows you how to add drawing area controls to a customized store locator using REST API queries. The REST API used returns 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 drawing is enabled, the stores are loaded only for the region inside the drawn area.

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.

Store Locator using REST API Queries

To see how to create a store locator like the example above check out the guide Store Locator using REST API Queries as Data Source or download the files needed for this step:

JavaScript
HTML

Changing Map Initialization Options

We have changed the map style, startup location and zoom level using options available from gem.core.App.initAppScreen:

1 const defaultAppScreen = gem.core.App.initAppScreen({
2     container: 'map-canvas',
3     zoom: 10,
4     center: [40.431404, -3.680445], // Madrid
5     style: './Printemps.style'
6 });

To see how to create your own custom map style check out the guide Create a Map Style.

Download Printemps Map Style

Changing the Markers and Markers Groups Style

To change the markers style you can use the options available in the control class gem.control.CustomQueryAddedDataControl and set your custom style rules:

 1 const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
 2     marker: {
 3             highlightClass: 'highlight-store-marker',
 4             cssClass: 'marker-circle',
 5             width: 12,
 6             height: 12,
 7             hoverWidth: 17,
 8             hoverHeight: 17,
 9             markerPos: 'center'
10     },
11     ...
12     markerGrouping: {
13             maxLevel: 14,
14             style: gem.control.MarkersGroupStyleType.custom,
15             markerGroupFunction: function (groupsize) {
16                     const customGroupDiv = document.createElement('div');
17                     customGroupDiv.classList.add('marker-circle', 'marker-group', 'disable-pointer-events');
18
19                     const textDiv = customGroupDiv.appendChild(document.createElement('div'));
20                     textDiv.className = 'marker-group-text';
21                     const textGroupSize = textDiv.appendChild(document.createElement('span'));
22                     textGroupSize.textContent = groupsize;
23
24                     return customGroupDiv;
25             }
26     }
27 ...
28 });
 1 <style>
 2   .marker-circle {
 3     width: 12px;
 4     height: 12px;
 5     border-radius: 50%;
 6     border: 2px solid #fff;
 7     background-color: #04aa6d;
 8     position: absolute;
 9     -webkit-box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
10     box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
11     cursor: pointer;
12   }
13
14   .marker-group {
15     width: 14px;
16     height: 14px;
17     background-color: #2e7d32;
18   }
19   .marker-group-text {
20     display: inline-block;
21     position: relative;
22     border-radius: 50%;
23     max-width: 30px;
24     min-width: 18px;
25     min-height: 10px;
26     text-align: center;
27     font-size: 0.7rem;
28     padding: 0.1rem 0rem 0.1rem 0rem;
29     top: 4px;
30     left: 50%;
31     -webkit-touch-callout: none;
32     -webkit-user-select: none;
33     -khtml-user-select: none;
34     -moz-user-select: none;
35     -ms-user-select: none;
36     user-select: none;
37     background-color: white;
38     -webkit-box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
39     box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
40   }
41
42   .highlight-store-marker {
43     filter: hue-rotate(80deg) contrast(1.5);
44   }
45
46   .disable-pointer-events {
47     pointer-events: none;
48     cursor: pointer;
49   }
50 ...
51 </style>

Adding Draw Area Control

 1 const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
 2     marker: {
 3     ...
 4 }, {
 5     markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
 6     ...
 7     },
 8     markersAreaSelectionFunction: async function (area, filters, signal) {
 9             const strUrlParams = filtersToParams(filters);
10             const strUrlAreaParams = areaCoordsToParams(area);
11             const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' + strUrlAreaParams + strUrlParams;
12
13             return fetchGeoJSONData(dataFetchUrl, { method: 'GET', signal: signal })
14                     .then(response => { return response; })
15                     .catch((e) => { return ''; });
16     }
17 });
18 defaultAppScreen.addControl(dataQueryControl);
19
20 const drawAreaControl = new gem.control.DrawAreaOnMapControl(dataQueryControl, {
21     button: {
22             parentContainer: 'draw-button-container'
23     }
24 });
25 defaultAppScreen.addControl(drawAreaControl);
26
27 function areaCoordsToParams(area) {
28     let strParams = 'polygon=';
29     area.forEach((elem, index) => {
30             strParams += elem.longitude + ',' + elem.latitude;
31             if (index < area.length - 1)
32                     strParams += ',';
33     });
34     const strUrlParams = encodeURI(strParams);
35     return strUrlParams;
36 }
37
38 function fetchGeoJSONData(url, options) {
39     return fetch(url, options)
40             .then(response => response.json())
41             .then(responseJson => {
42                     return jsonToGeoJSON(responseJson);
43             })
44             .catch((e) => {
45                     return '';
46             });
47 }
48
49 function jsonToGeoJSON(jsonData) {
50     jsonData.forEach(element => {
51             const propertiesJson = JSON.parse(element.properties);
52             for (const [key, value] of Object.entries(propertiesJson.properties)) {
53                     element[key] = value;
54             }
55             delete element.properties;
56     });
57     const GeoJSONdata = GeoJSON.parse(jsonData, { Point: ['st_y', 'st_x'] });
58     return GeoJSONdata;
59 }
The control class gem.control.DrawAreaOnMapControl is used to add a draw area control over the interactive map.
The start/stop drawing button is appended to the parent element specified in the option button.parentContainer.

Using the option markersAreaSelectionFunction from data source control class gem.control.CustomQueryAddedDataControl you specify how to handle the data source REST API requests in the case of an area selection.

In our example, the function areaCoordsToParams() is used to transform the drawn area contour coordinates into the URL parameters needed by the REST API.

You can customize the draw area control using the options available for the button style and text, dialog style and text, the draw area stroke and fill, as well as other options.
 1 <style>
 2 ...
 3   .draw-button-container {
 4     position: absolute;
 5     left: 27%;
 6     top: 8px;
 7   }
 8 </style>
 9
10 <body>
11     <div id="store-locator" style="width: 100%; height: 100%">
12       <div id="stores-list" class="menu-list-container" style="width: 25%; height: 100%; position: absolute"></div>
13       <div id="map-canvas" style="width: 75%; left: 25%; height: 100%; position: absolute; overflow: hidden"></div>
14
15       <div id="draw-button-container" class="draw-button-container"></div>
16
17       <div id="filters-container" class="filters-container">
18         <div id="filter-languages" class="filter-languages"></div>
19         <div id="filter-limit" class="filter-limit"></div>
20       </div>
21     </div>
22 ...
23 </body>
The div with id draw-button-container is the parent element for the draw button element.

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     zoom: 10,
  7     center: [40.431404, -3.680445], // Madrid
  8     style: './Printemps.style'
  9 });
 10
 11 const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
 12     marker: {
 13             highlightClass: 'highlight-store-marker',
 14             cssClass: 'marker-circle',
 15             width: 12,
 16             height: 12,
 17             hoverWidth: 17,
 18             hoverHeight: 17,
 19             markerPos: 'center'
 20     },
 21     markerBubble: {
 22             title: ['name'],
 23             image: ['preview']
 24     },
 25     markerGrouping: {
 26             maxLevel: 14,
 27             style: gem.control.MarkersGroupStyleType.custom,
 28             markerGroupFunction: function (groupsize) {
 29                     const customGroupDiv = document.createElement('div');
 30                     customGroupDiv.classList.add('marker-circle', 'marker-group', 'disable-pointer-events');
 31
 32                     const textDiv = customGroupDiv.appendChild(document.createElement('div'));
 33                     textDiv.className = 'marker-group-text';
 34                     const textGroupSize = textDiv.appendChild(document.createElement('span'));
 35                     textGroupSize.textContent = groupsize;
 36
 37                     return customGroupDiv;
 38             }
 39     }
 40 }, {
 41     markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
 42             const strUrlParams = filtersToParams(filters);
 43
 44             const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
 45                     'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + strUrlParams;
 46
 47             return fetchGeoJSONData(dataFetchUrl, { method: 'GET', signal: signal })
 48                     .then(response => { return response; })
 49                     .catch((e) => { return ''; });
 50     },
 51     markersAreaSelectionFunction: async function (area, filters, signal) {
 52             const strUrlParams = filtersToParams(filters);
 53             const strUrlAreaParams = areaCoordsToParams(area);
 54             const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' + strUrlAreaParams + strUrlParams;
 55
 56             return fetchGeoJSONData(dataFetchUrl, { method: 'GET', signal: signal })
 57                     .then(response => { return response; })
 58                     .catch((e) => { return ''; });
 59     }
 60 });
 61 defaultAppScreen.addControl(dataQueryControl);
 62
 63 const drawAreaControl = new gem.control.DrawAreaOnMapControl(dataQueryControl, {
 64     button: {
 65             parentContainer: 'draw-button-container'
 66     }
 67 });
 68 defaultAppScreen.addControl(drawAreaControl);
 69
 70 const radioLimitFilter = new gem.control.CategoryFilterControl({
 71     parentContainer: 'filter-limit',
 72     name: 'byLimit',
 73     title: 'Filter by Limit',
 74     categories: [
 75             { label: '100 items', filter: { key: 'limit', value: '100' } },
 76             { label: '500 items', filter: { key: 'limit', value: '500' } },
 77             { label: '1000 items', filter: { key: 'limit', value: '1000' } }
 78     ]
 79 }, dataQueryControl);
 80
 81 const checkboxesLanguageFilterControl = new gem.control.CategoryFilterControl({
 82     parentContainer: 'filter-languages',
 83     name: 'byLanguage',
 84     style: 'checkboxes',
 85     title: 'Filter by Language',
 86     categories: [
 87             {
 88                     label: 'All', filter: { key: 'langs', value: '' }, children:
 89                             [
 90                                     { label: 'English', filter: { key: 'langs', value: 'en' } },
 91                                     { label: 'Dutch', filter: { key: 'langs', value: 'nl' } },
 92                                     { label: 'Finnish', filter: { key: 'langs', value: 'fi' } },
 93                                     { label: 'Romanian', filter: { key: 'langs', value: 'ro' } },
 94                                     { label: 'Spanish', filter: { key: 'langs', value: 'es' } },
 95                                     { label: 'German', filter: { key: 'langs', value: 'de' } },
 96                                     { label: 'Hungarian', filter: { key: 'langs', value: 'hu' } }
 97                             ]
 98             }
 99     ]
100 }, dataQueryControl);
101 defaultAppScreen.addControl(checkboxesLanguageFilterControl);
102 defaultAppScreen.addControl(radioLimitFilter);
103
104
105 const searchControl = new gem.control.SearchControl({
106     highlightOptions: {
107             contourColor: { r: 0, g: 255, b: 0, a: 0 }
108     },
109     searchPreferences: {
110             exactMatch: true,
111             maximumMatches: 3,
112             addressSearch: true,
113             mapPoisSearch: true,
114             setCursorReferencePoint: true
115     }
116 });
117 defaultAppScreen.addControl(searchControl);
118
119 const listUIControl = new gem.control.ListControl({
120     sourceControl: dataQueryControl,
121     container: 'stores-list',
122     displayCount: true,
123     flyToItemAltitude: 300,
124     menuName: 'REST API data source',
125     titleProperties: ['name'],
126     detailsProperties: ['language'],
127     imageProperty: ['preview']
128 });
129 defaultAppScreen.addControl(listUIControl);
130
131 function filtersToParams(filters) {
132     let strParams = '';
133     for (const [name, filterValues] of filters.entries()) {
134             if (name === 'byLimit') {
135                     strParams += '&' + filterValues[0].key + '=' + filterValues[0].value;
136             }
137             else if (name === 'byLanguage') {
138                     if (!filterValues.length) {
139                             strParams += '&langs=all';
140                             continue;
141                     }
142                     strParams += '&' + filterValues[0].key + '=';
143                     for (let i = 0; i < filterValues.length; i++) {
144                             strParams += filterValues[i].value + ',';
145                     }
146             }
147     }
148     if (!filters.has('byLimit')) {
149             strParams += '&limit=100';
150     }
151     if (!filters.has('byLanguage')) {
152             strParams += '&langs=all';
153     }
154     const strUrlParams = encodeURI(strParams);
155     return strUrlParams;
156 }
157
158 function areaCoordsToParams(area) {
159     let strParams = 'polygon=';
160     area.forEach((elem, index) => {
161             strParams += elem.longitude + ',' + elem.latitude;
162             if (index < area.length - 1)
163                     strParams += ',';
164     });
165     const strUrlParams = encodeURI(strParams);
166     return strUrlParams;
167 }
168
169 function fetchGeoJSONData(url, options) {
170     return fetch(url, options)
171             .then(response => response.json())
172             .then(responseJson => {
173                     return jsonToGeoJSON(responseJson);
174             })
175             .catch((e) => {
176                     return '';
177             });
178 }
179
180 function jsonToGeoJSON(jsonData) {
181     jsonData.forEach(element => {
182             const propertiesJson = JSON.parse(element.properties);
183             for (const [key, value] of Object.entries(propertiesJson.properties)) {
184                     element[key] = value;
185             }
186             delete element.properties;
187     });
188     const GeoJSONdata = GeoJSON.parse(jsonData, { Point: ['st_y', 'st_x'] });
189     return GeoJSONdata;
190 }
 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 Draw Area control - 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      .marker-circle {
12        width: 12px;
13        height: 12px;
14        border-radius: 50%;
15        border: 2px solid #fff;
16        background-color: #04aa6d;
17        position: absolute;
18        -webkit-box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
19        box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
20        cursor: pointer;
21      }
22
23      .marker-group {
24        width: 14px;
25        height: 14px;
26        background-color: #2e7d32;
27      }
28      .marker-group-text {
29        display: inline-block;
30        position: relative;
31        border-radius: 50%;
32        max-width: 30px;
33        min-width: 18px;
34        min-height: 10px;
35        text-align: center;
36        font-size: 0.7rem;
37        padding: 0.1rem 0rem 0.1rem 0rem;
38        top: 4px;
39        left: 50%;
40        -webkit-touch-callout: none;
41        -webkit-user-select: none;
42        -khtml-user-select: none;
43        -moz-user-select: none;
44        -ms-user-select: none;
45        user-select: none;
46        background-color: white;
47        -webkit-box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
48        box-shadow: 0px 5px 15px rgb(0 0 0 / 15%);
49      }
50
51      .highlight-store-marker {
52        filter: hue-rotate(80deg) contrast(1.5);
53      }
54
55      .disable-pointer-events {
56        pointer-events: none;
57        cursor: pointer;
58      }
59
60      .filters-container {
61        position: absolute;
62        width: auto;
63        max-width: 300px;
64        right: 0px;
65        top: 50px;
66        margin: 8px;
67        z-index: 9;
68        font-size: 0.75rem;
69        background-color: white;
70        box-shadow: 0 2px 4px rgb(0 0 0 / 20%), 0 -1px 0 rgb(0 0 0 / 2%);
71      }
72
73      .draw-button-container {
74        position: absolute;
75        left: 27%;
76        top: 8px;
77      }
78    </style>
79  </head>
80
81  <body>
82    <div id="store-locator" style="width: 100%; height: 100%">
83      <div id="stores-list" class="menu-list-container" style="width: 25%; height: 100%; position: absolute"></div>
84      <div id="map-canvas" style="width: 75%; left: 25%; height: 100%; position: absolute; overflow: hidden"></div>
85
86      <div id="draw-button-container" class="draw-button-container"></div>
87
88      <div id="filters-container" class="filters-container">
89        <div id="filter-languages" class="filter-languages"></div>
90        <div id="filter-limit" class="filter-limit"></div>
91      </div>
92    </div>
93
94    <script src="https://www.magiclane.com/sdk/js/gemapi.js"></script>
95    <script type="text/javascript" src="token.js"></script>
96    <script type="text/javascript" src="geojson.min.js"></script>
97    <script type="text/javascript" src="storeRESTQueryDrawArea.js"></script>
98  </body>
99</html>

Files

Source code for this example:

JavaScript
HTML
Printemps Map Style

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