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 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);
The places free text search control is initialized using the control class
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);
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:
JavaScriptHTML
right-click on the links and select Save As.
For transforming JSON data into GeoJSON we are using geojson.js