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:
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
- JavaScript
- HTML
// Start by setting your token from https://developer.magiclane.com/api/projects
if (gem.core.App.token === undefined) gem.core.App.token = '';
const defaultAppScreen = gem.core.App.initAppScreen({
container: 'map-canvas',
center: [48.8562, 2.3516, 10000] // Paris
});
const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
markerBubble: {
title: ['name'],
image: ['preview']
},
markerGrouping: {
maxLevel: 14,
style: gem.control.MarkersGroupStyleType.default
}
}, {
markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + '&limit=1000&langs=all';
return fetch(dataFetchUrl, { method: 'GET', signal: signal })
.then(response => response.json())
.then(responseJson => {
return jsonToGeoJSON(responseJson);
})
.catch((e) => {
return '';
});
}
});
defaultAppScreen.addControl(dataQueryControl);
function jsonToGeoJSON(jsonData) {
jsonData.forEach(element => {
const propertiesJson = JSON.parse(element.properties);
for (const [key, value] of Object.entries(propertiesJson.properties)) {
element[key] = value;
}
delete element.properties;
});
const GeoJSONdata = GeoJSON.parse(jsonData, { Point: ['st_y', 'st_x'] });
return GeoJSONdata;
}
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()
.
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8" />
<title>Store Locator REST API data source - MagicLane Maps SDK for JavaScript</title>
<link rel="stylesheet" type="text/css" href="https://www.magiclane.com/sdk/js/gem.css" />
<link rel="stylesheet" href="/fonts/webfonts.css" type="text/css" media="all" />
</head>
<body>
<div id="store-locator" style="width: 100%; height: 100%">
<div id="map-canvas" style="width: 100%; height: 100%; position: absolute; overflow: hidden"></div>
</div>
<script src="https://www.magiclane.com/sdk/js/gemapi.js"></script>
<script type="text/javascript" src="token.js"></script>
<script type="text/javascript" src="geojson.min.js"></script>
<script type="text/javascript" src="storeRESTQueryRequests01.js"></script>
</body>
</html>
Adding free text search
To add free-form text search functionality simply add the following JavaScript code:
- JavaScript
const searchControl = new gem.control.SearchControl({
highlightOptions: {
contourColor: { r: 0, g: 255, b: 0, a: 0 }
},
searchPreferences: {
exactMatch: true,
maximumMatches: 3,
addressSearch: true,
mapPoisSearch: true,
setCursorReferencePoint: true
}
});
defaultAppScreen.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
- JavaScript
- HTML
const listUIControl = new gem.control.ListControl({
sourceControl: dataQueryControl,
container: 'stores-list',
displayCount: true,
flyToItemAltitude: 300,
menuName: 'REST API data source',
titleProperties: ['name'],
detailsProperties: ['language'],
imageProperty: ['preview']
});
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.
...
<body>
<div id="store-locator" style="width: 100%; height: 100%">
<div id="stores-list" class="menu-list-container" style="width: 25%; height: 100%; position: absolute"></div>
<div id="map-canvas" style="width: 75%; left: 25%; height: 100%; position: absolute; overflow: hidden"></div>
</div>
...
Adding filters control
To add filters for your data you can follow the boilerplate code below:
- JavaScript
- HTML
const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
markerBubble: {
title: ['name'],
image: ['preview']
},
markerGrouping: {
maxLevel: 14,
style: gem.control.MarkersGroupStyleType.default
}
}, {
markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
const strUrlParams = filtersToUrlParams(filters);
const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + strUrlParams;
return fetch(dataFetchUrl, { method: 'GET', signal: signal })
.then(response => response.json())
.then(responseJson => {
return jsonToGeoJSON(responseJson);
})
.catch((e) => {
return '';
});
}
});
defaultAppScreen.addControl(dataQueryControl);
const radioLimitFilter = new gem.control.CategoryFilterControl({
parentContainer: 'filter-category-limit',
name: 'byLimit',
title: 'Limit:',
categories: [
{ label: '100 items', filter: { key: 'limit', value: '100' } },
{ label: '500 items', filter: { key: 'limit', value: '500' } },
{ label: '1000 items', filter: { key: 'limit', value: '1000' } }
]
}, dataQueryControl);
const checkboxesLanguageFilterControl = new gem.control.CategoryFilterControl({
parentContainer: 'filter-category-language',
name: 'byLanguage',
style: 'checkboxes',
title: 'Filter by Languages',
categories: [
{
label: 'All', filter: { key: 'langs', value: '' }, children:
[
{ label: 'English', filter: { key: 'langs', value: 'en' } },
{ label: 'Dutch', filter: { key: 'langs', value: 'nl' } },
{ label: 'Finnish', filter: { key: 'langs', value: 'fi' } },
{ label: 'Romanian', filter: { key: 'langs', value: 'ro' } },
{ label: 'Spanish', filter: { key: 'langs', value: 'es' } },
{ label: 'German', filter: { key: 'langs', value: 'de' } },
{ label: 'Hungarian', filter: { key: 'langs', value: 'hu' } }
]
}
]
}, dataQueryControl);
defaultAppScreen.addControl(checkboxesLanguageFilterControl);
defaultAppScreen.addControl(radioLimitFilter);
function filtersToUrlParams(filters) {
let strParams = '';
for (const [name, filterValues] of filters.entries()) {
if (name === 'byLimit') {
strParams += '&' + filterValues[0].key + '=' + filterValues[0].value;
}
else if (name === 'byLanguage') {
if (!filterValues.length) {
strParams += '&langs=all';
continue;
}
strParams += '&' + filterValues[0].key + '=';
for (let i = 0; i < filterValues.length; i++) {
strParams += filterValues[i].value + ',';
}
}
}
if (!filters.has('byLimit')) {
strParams += '&limit=100';
}
if (!filters.has('byLanguage')) {
strParams += '&langs=all';
}
const strUrlParams = encodeURI(strParams);
return strUrlParams;
}
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
.
...
<style>
.filters-container {
position: absolute;
width: auto;
max-width: 300px;
right: 0px;
top: 50px;
margin: 8px;
z-index: 9;
font-size: 0.7rem;
background-color: white;
box-shadow: 0 2px 4px rgb(0 0 0 / 20%), 0 -1px 0 rgb(0 0 0 / 2%);
}
...
<div id="filters-container" class="filters-container">
<div id="filter-category-language" class="filter-category-language"></div>
<div id="filter-category-limit" class="filter-category-limit"></div>
</div>
...
Complete example code
- JavaScript
- HTML
// Start by setting your token from https://developer.magiclane.com/api/projects
if (gem.core.App.token === undefined) gem.core.App.token = '';
const defaultAppScreen = gem.core.App.initAppScreen({
container: 'map-canvas',
center: [48.8562, 2.3516, 10000] // Paris
});
const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
markerBubble: {
title: ['name'],
image: ['preview']
},
markerGrouping: {
maxLevel: 14,
style: gem.control.MarkersGroupStyleType.default
}
}, {
markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
const strUrlParams = filtersToUrlParams(filters);
const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + strUrlParams;
return fetch(dataFetchUrl, { method: 'GET', signal: signal })
.then(response => response.json())
.then(responseJson => {
return jsonToGeoJSON(responseJson);
})
.catch((e) => {
return '';
});
}
});
defaultAppScreen.addControl(dataQueryControl);
const radioLimitFilter = new gem.control.CategoryFilterControl({
parentContainer: 'filter-category-limit',
name: 'byLimit',
title: 'Filter by Limit',
categories: [
{ label: '100 items', filter: { key: 'limit', value: '100' } },
{ label: '500 items', filter: { key: 'limit', value: '500' } },
{ label: '1000 items', filter: { key: 'limit', value: '1000' } }
]
}, dataQueryControl);
const checkboxesLanguageFilterControl = new gem.control.CategoryFilterControl({
parentContainer: 'filter-category-language',
name: 'byLanguage',
style: 'checkboxes',
title: 'Filter by Languages',
categories: [
{
label: 'All', filter: { key: 'langs', value: '' }, children:
[
{ label: 'English', filter: { key: 'langs', value: 'en' } },
{ label: 'Dutch', filter: { key: 'langs', value: 'nl' } },
{ label: 'Finnish', filter: { key: 'langs', value: 'fi' } },
{ label: 'Romanian', filter: { key: 'langs', value: 'ro' } },
{ label: 'Spanish', filter: { key: 'langs', value: 'es' } },
{ label: 'German', filter: { key: 'langs', value: 'de' } },
{ label: 'Hungarian', filter: { key: 'langs', value: 'hu' } }
]
}
]
}, dataQueryControl);
defaultAppScreen.addControl(checkboxesLanguageFilterControl);
defaultAppScreen.addControl(radioLimitFilter);
const searchControl = new gem.control.SearchControl({
highlightOptions: {
contourColor: { r: 0, g: 255, b: 0, a: 0 }
},
searchPreferences: {
exactMatch: true,
maximumMatches: 3,
addressSearch: true,
mapPoisSearch: true,
setCursorReferencePoint: true
}
});
defaultAppScreen.addControl(searchControl);
const listUIControl = new gem.control.ListControl({
sourceControl: dataQueryControl,
container: 'stores-list',
displayCount: true,
flyToItemAltitude: 300,
menuName: 'REST API data source',
titleProperties: ['name'],
detailsProperties: ['language'],
imageProperty: ['preview']
});
defaultAppScreen.addControl(listUIControl);
function filtersToUrlParams(filters) {
let strParams = '';
for (const [name, filterValues] of filters.entries()) {
if (name === 'byLimit') {
strParams += '&' + filterValues[0].key + '=' + filterValues[0].value;
}
else if (name === 'byLanguage') {
if (!filterValues.length) {
strParams += '&langs=all';
continue;
}
strParams += '&' + filterValues[0].key + '=';
for (let i = 0; i < filterValues.length; i++) {
strParams += filterValues[i].value + ',';
}
}
}
if (!filters.has('byLimit')) {
strParams += '&limit=100';
}
if (!filters.has('byLanguage')) {
strParams += '&langs=all';
}
const strUrlParams = encodeURI(strParams);
return strUrlParams;
}
function jsonToGeoJSON(jsonData) {
jsonData.forEach(element => {
const propertiesJson = JSON.parse(element.properties);
for (const [key, value] of Object.entries(propertiesJson.properties)) {
element[key] = value;
}
delete element.properties;
});
const GeoJSONdata = GeoJSON.parse(jsonData, { Point: ['st_y', 'st_x'] });
return GeoJSONdata;
}
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no, shrink-to-fit=no" />
<title>Store Locator REST API data source - MagicLane Maps SDK for JavaScript</title>
<link rel="stylesheet" type="text/css" href="https://www.magiclane.com/sdk/js/gem.css" />
<link rel="stylesheet" href="/fonts/webfonts.css" type="text/css" media="all" />
<style>
.filters-container {
position: absolute;
width: auto;
max-width: 300px;
right: 0px;
top: 50px;
margin: 8px;
z-index: 9;
font-size: 0.7rem;
background-color: white;
box-shadow: 0 2px 4px rgb(0 0 0 / 20%), 0 -1px 0 rgb(0 0 0 / 2%);
}
</style>
</head>
<body>
<div id="store-locator" style="width: 100%; height: 100%">
<div id="stores-list" class="menu-list-container" style="width: 25%; height: 100%; position: absolute"></div>
<div id="map-canvas" style="width: 75%; left: 25%; height: 100%; position: absolute; overflow: hidden"></div>
<div id="filters-container" class="filters-container">
<div id="filter-category-language" class="filter-category-language"></div>
<div id="filter-category-limit" class="filter-category-limit"></div>
</div>
</div>
<script src="https://www.magiclane.com/sdk/js/gemapi.js"></script>
<script type="text/javascript" src="token.js"></script>
<script type="text/javascript" src="geojson.min.js"></script>
<script type="text/javascript" src="storeRESTQueryRequests.js"></script>
</body>
</html>
Files
Source code for this example:
Right-click on the links and select Save As.
For transforming JSON data into GeoJSON we are using geojson.js.
Related
See Store Locator using GeoJSON File as Data Source with Custom Controls for an example with GeoJSON data loaded from a file and using custom controls.
See Store Locator using a GeoJSON File as Data Source for an example with GeoJSON data loaded from a file and rendered on the map.
See Store Locator using Studio Defined Data Source for an example with GeoJSON data uploaded to the map studio and rendered on the map.
JavaScript Examples
Maps SDK for JavaScript Examples can be downloaded or cloned with Git.