Skip to main content
GuidesAPI ReferenceExamples

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

Estimated reading time: 11 minutes

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.

info

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:

Changing Map Initialization Options

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

// Start by setting your token from https://developer.magiclane.com/api/projects
const defaultAppScreen = gem.core.App.initAppScreen({
container: 'map-canvas',
zoom: 10,
center: [40.431404, -3.680445], // Madrid
style: './Printemps.style'
});

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:

const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
marker: {
highlightClass: 'highlight-store-marker',
cssClass: 'marker-circle',
width: 12,
height: 12,
hoverWidth: 17,
hoverHeight: 17,
markerPos: 'center'
},
...
markerGrouping: {
maxLevel: 14,
style: gem.control.MarkersGroupStyleType.custom,
markerGroupFunction: function (groupsize) {
const customGroupDiv = document.createElement('div');
customGroupDiv.classList.add('marker-circle', 'marker-group', 'disable-pointer-events');

const textDiv = customGroupDiv.appendChild(document.createElement('div'));
textDiv.className = 'marker-group-text';
const textGroupSize = textDiv.appendChild(document.createElement('span'));
textGroupSize.textContent = groupsize;

return customGroupDiv;
}
}
...
});

Adding Draw Area Control

const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
marker: {
...
}, {
markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
...
},
markersAreaSelectionFunction: async function (area, filters, signal) {
const strUrlParams = filtersToParams(filters);
const strUrlAreaParams = areaCoordsToParams(area);
const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' + strUrlAreaParams + strUrlParams;

return fetchGeoJSONData(dataFetchUrl, { method: 'GET', signal: signal })
.then(response => { return response; })
.catch((e) => { return ''; });
}
});
defaultAppScreen.addControl(dataQueryControl);

const drawAreaControl = new gem.control.DrawAreaOnMapControl(dataQueryControl, {
button: {
parentContainer: 'draw-button-container'
}
});
defaultAppScreen.addControl(drawAreaControl);

function areaCoordsToParams(area) {
let strParams = 'polygon=';
area.forEach((elem, index) => {
strParams += elem.longitude + ',' + elem.latitude;
if (index < area.length - 1)
strParams += ',';
});
const strUrlParams = encodeURI(strParams);
return strUrlParams;
}

function fetchGeoJSONData(url, options) {
return fetch(url, options)
.then(response => response.json())
.then(responseJson => {
return jsonToGeoJSON(responseJson);
})
.catch((e) => {
return '';
});
}

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 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.

Complete example code

// 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',
zoom: 10,
center: [40.431404, -3.680445], // Madrid
style: './Printemps.style'
});

const dataQueryControl = new gem.control.CustomQueryAddedDataControl('' /* default icon */, '' /* icon filter */, {
marker: {
highlightClass: 'highlight-store-marker',
cssClass: 'marker-circle',
width: 12,
height: 12,
hoverWidth: 17,
hoverHeight: 17,
markerPos: 'center'
},
markerBubble: {
title: ['name'],
image: ['preview']
},
markerGrouping: {
maxLevel: 14,
style: gem.control.MarkersGroupStyleType.custom,
markerGroupFunction: function (groupsize) {
const customGroupDiv = document.createElement('div');
customGroupDiv.classList.add('marker-circle', 'marker-group', 'disable-pointer-events');

const textDiv = customGroupDiv.appendChild(document.createElement('div'));
textDiv.className = 'marker-group-text';
const textGroupSize = textDiv.appendChild(document.createElement('span'));
textGroupSize.textContent = groupsize;

return customGroupDiv;
}
}
}, {
markersFeedFunction: async function (mapCenterLon, mapCenterlat, mapLonMin, mapLatMin, mapLonMax, mapLatMax, filters, signal) {
const strUrlParams = filtersToParams(filters);

const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' +
'polygon=' + mapLonMin + ',' + mapLatMin + ',' + mapLonMax + ',' + mapLatMin + ',' + mapLonMin + ',' + mapLatMax + ',' + mapLonMax + ',' + mapLatMax + strUrlParams;

return fetchGeoJSONData(dataFetchUrl, { method: 'GET', signal: signal })
.then(response => { return response; })
.catch((e) => { return ''; });
},
markersAreaSelectionFunction: async function (area, filters, signal) {
const strUrlParams = filtersToParams(filters);
const strUrlAreaParams = areaCoordsToParams(area);
const dataFetchUrl = 'https://acq1.ro.m71os.services.generalmagic.com/storelocator/query_external?' + strUrlAreaParams + strUrlParams;

return fetchGeoJSONData(dataFetchUrl, { method: 'GET', signal: signal })
.then(response => { return response; })
.catch((e) => { return ''; });
}
});
defaultAppScreen.addControl(dataQueryControl);

const drawAreaControl = new gem.control.DrawAreaOnMapControl(dataQueryControl, {
button: {
parentContainer: 'draw-button-container'
}
});
defaultAppScreen.addControl(drawAreaControl);

const radioLimitFilter = new gem.control.CategoryFilterControl({
parentContainer: 'filter-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-languages',
name: 'byLanguage',
style: 'checkboxes',
title: 'Filter by Language',
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 filtersToParams(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 areaCoordsToParams(area) {
let strParams = 'polygon=';
area.forEach((elem, index) => {
strParams += elem.longitude + ',' + elem.latitude;
if (index < area.length - 1)
strParams += ',';
});
const strUrlParams = encodeURI(strParams);
return strUrlParams;
}

function fetchGeoJSONData(url, options) {
return fetch(url, options)
.then(response => response.json())
.then(responseJson => {
return jsonToGeoJSON(responseJson);
})
.catch((e) => {
return '';
});
}

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;
}

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

JavaScript Examples

Maps SDK for JavaScript Examples can be downloaded or cloned with Git