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.
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}
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.
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 free text search ¶
To add free-form text search functionality simply add the following JavaScript code:
1const searchControl = new gem.control.SearchControl({ 2 highlightOptions: { 3 contourColor: { r: 0, g: 255, b: 0, a: 0 } 4 }, 5 searchPreferences: { 6 exactMatch: true, 7 maximumMatches: 3, 8 addressSearch: true, 9 mapPoisSearch: true, 10 setCursorReferencePoint: true 11 } 12}); 13defaultAppScreen.addControl(searchControl);
gem.control.SearchControl
.
The functionality and appearance can be customized using the class options.
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);
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
.
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 }
gem.control.CategoryFilterControl
.
radioLimitFilter
is used for limiting the number of results shown on the map and is using
the
radio
style to display the available options.
checkboxesLanguageFilterControl
is used for filtering the data based on the language of
the items and is using the
checkboxes
style to display the options.
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:
JavaScriptHTML
right-click on the links and select Save As.
For transforming JSON data into GeoJSON we are using geojson.js