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.
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:
JavaScriptHTML
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 StyleChanging 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 }
gem.control.DrawAreaOnMapControl
is used to add a draw area control over
the interactive map.button.parentContainer
.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.areaCoordsToParams()
is used to transform the drawn area contour coordinates into the
URL parameters needed by the REST API. 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>
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:
JavaScriptHTML
Printemps Map Style
right-click on the links and select Save As.
For transforming JSON data into GeoJSON we are using geojson.js