Josie's Cafe is a fictitious restaurant chain featuring two different location
categories: coffee shops, and patisseries. This tutorial shows you how to build
a store locator map application that displays custom markers to indicate store
locations. It demonstrates how to do the following things:
Use GeoJSON to define a static list of locations and associated metadata.
Display location markers on a map, using icons to indicate category (cafe
or patisserie).
Display location metadata using a custom InfoWindow.
/*
* Copyright 2017 Google Inc. All rights reserved.
*
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
// Style credit: https://snazzymaps.com/style/1/pale-dawn
const mapStyle = [
{
"featureType": "administrative",
"elementType": "all",
"stylers": [
{
"visibility": "on"
},
{
"lightness": 33
}
]
},
{
"featureType": "landscape",
"elementType": "all",
"stylers": [
{
"color": "#f2e5d4"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#c5dac6"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels",
"stylers": [
{
"visibility": "on"
},
{
"lightness": 20
}
]
},
{
"featureType": "road",
"elementType": "all",
"stylers": [
{
"lightness": 20
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#c5c6c6"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry",
"stylers": [
{
"color": "#e4d7c6"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry",
"stylers": [
{
"color": "#fbfaf7"
}
]
},
{
"featureType": "water",
"elementType": "all",
"stylers": [
{
"visibility": "on"
},
{
"color": "#acbcc9"
}
]
}
];
// Escapes HTML characters in a template literal string, to prevent XSS.
// See https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content
function sanitizeHTML(strings) {
const entities = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''};
let result = strings[0];
for (let i = 1; i < arguments.length; i++) {
result += String(arguments[i]).replace(/[&<>'"]/g, (char) => {
return entities[char];
});
result += strings[i];
}
return result;
}
function initMap() {
// Create the map.
const map = new google.maps.Map(document.getElementsByClassName('map')[0], {
zoom: 7,
center: {lat: 52.632469, lng: -1.689423},
styles: mapStyle
});
// Load the stores GeoJSON onto the map.
map.data.loadGeoJson('stores.json');
// Define the custom marker icons, using the store's "category".
map.data.setStyle(feature => {
return {
icon: {
url: `img/icon_${feature.getProperty('category')}.png`,
scaledSize: new google.maps.Size(64, 64)
}
};
});
const apiKey = 'YOUR_API_KEY';
const infoWindow = new google.maps.InfoWindow();
infoWindow.setOptions({pixelOffset: new google.maps.Size(0, -30)});
// Show the information for a store when its marker is clicked.
map.data.addListener('click', event => {
const category = event.feature.getProperty('category');
const name = event.feature.getProperty('name');
const description = event.feature.getProperty('description');
const hours = event.feature.getProperty('hours');
const phone = event.feature.getProperty('phone');
const position = event.feature.getGeometry().get();
const content = sanitizeHTML`
<img style="float:left; width:200px; margin-top:30px" src="img/logo_${category}.png">
<div style="margin-left:220px; margin-bottom:20px;">
<h2>${name}</h2><p>${description}</p>
<p><b>Open:</b> ${hours}<br/><b>Phone:</b> ${phone}</p>
<p><img src="https://maps.googleapis.com/maps/api/streetview?size=350x120&location=${position.lat()},${position.lng()}&key=${apiKey}"></p>
</div>
`;
infoWindow.setContent(content);
infoWindow.setPosition(position);
infoWindow.open(map);
});
}
< > Show/Hide index.html
<!--
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Store Locator</title>
<style>
.map {height: 100%;}
html, body {height: 100%; margin: 0; padding: 0;}
</style>
<head>
<body>
<div class="map"></div>
<script src="app.js"></script>
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
</script>
</body>
</html>
< > Show/Hide stores.json
{
"type": "FeatureCollection",
"features": [
{
"geometry": {
"type": "Point",
"coordinates": [
-0.145365,
51.506182
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "Modern twists on classic pastries. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Mayfair",
"phone": "+44 20 1234 5678"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-2.579623,
51.452251
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "Come and try our award-winning cakes and pastries. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Bristol",
"phone": "+44 117 121 2121"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
1.273459,
52.638072
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "Whatever the occasion, whether it's a birthday or a wedding, Josie's Patisserie has the perfect treat for you. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Norwich",
"phone": "+44 1603 123456"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-1.882509,
50.848831
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "A gourmet patisserie that will delight your senses. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Wimborne",
"phone": "+44 1202 343434"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-2.985933,
53.408899
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "Spoil yourself or someone special with our classic pastries. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Liverpool",
"phone": "+44 151 444 4444"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-1.689423,
52.632469
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "Come and feast your eyes and tastebuds on our delicious pastries and cakes. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Tamworth",
"phone": "+44 5555 55555"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-3.155305,
51.479756
]
},
"type": "Feature",
"properties": {
"category": "patisserie",
"hours": "10am - 6pm",
"description": "Josie's Patisserie is family-owned, and our delectable pastries, cakes, and great coffee are renowed. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Patisserie Cardiff",
"phone": "+44 29 6666 6666"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-0.725019,
52.668891
]
},
"type": "Feature",
"properties": {
"category": "cafe",
"hours": "8am - 9:30pm",
"description": "Oakham's favorite spot for fresh coffee and delicious cakes. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Cafe Oakham",
"phone": "+44 7777 777777"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-2.477653,
53.735405
]
},
"type": "Feature",
"properties": {
"category": "cafe",
"hours": "8am - 9:30pm",
"description": "Enjoy freshly brewed coffe, and home baked cakes in our homely cafe. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Cafe Blackburn",
"phone": "+44 8888 88888"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-0.211363,
51.108966
]
},
"type": "Feature",
"properties": {
"category": "cafe",
"hours": "8am - 9:30pm",
"description": "A delicious array of pastries with many flavours, and fresh coffee in an snug cafe. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Cafe Crawley",
"phone": "+44 1010 101010"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-0.123559,
50.832679
]
},
"type": "Feature",
"properties": {
"category": "cafe",
"hours": "8am - 9:30pm",
"description": "Grab a freshly brewed coffee, a decadent cake and relax in our idyllic cafe. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Cafe Brighton",
"phone": "+44 1313 131313"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
-3.319575,
52.517827
]
},
"type": "Feature",
"properties": {
"category": "cafe",
"hours": "8am - 9:30pm",
"description": "Come in and unwind at this idyllic cafe with fresh coffee and home made cakes. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Cafe Newtown",
"phone": "+44 1414 141414"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
1.158167,
52.071634
]
},
"type": "Feature",
"properties": {
"category": "cafe",
"hours": "8am - 9:30pm",
"description": "Fresh coffee and delicious cakes in an snug cafe. We're part of a larger chain of patisseries and cafes.",
"name": "Josie's Cafe Ipswich",
"phone": "+44 1717 17171"
}
}
]
}
The functionality for the Josie's Cafe store locator centers on these
files:
app.js contains the code for displaying the map and adding
markers.
index.html is a minimal HTML file that loads the Maps JavaScript API
and app.js, and displays the map in a div.
stores.json is a GeoJSON file that stores all of the data for each
location.
Set up your development project
Before starting this tutorial, follow these instructions to get an API
key and set up Google Cloud Platform.
Add the API key to your application
Add the API key to your project as follows (you'll need to get the code
before taking this step):
index.html — in the src attribute of the script tag, replace
YOUR_API_KEY with your actual API key:
app.js — Locate the following line and replace YOUR_API_KEY with
your actual API key:
const apiKey = 'YOUR_API_KEY';
Build and run your app
Follow these steps to build and run your app using Google Cloud Platform:
If you have not done so yet, clone the solution repo to
your GCP machine or local computer (these instructions assume that you're using
a GCP machine).
From the top-level directory, run the following command to start the Python
simple HTTP server:
$ python -m SimpleHTTPServer 8080
Click the first item on the Cloud Shell toolbar, and select Preview on
port 8080. A new browser tab opens, displaying a map centered on London,
England, with several markers to indicate the locations for Josie's Cafe.
Understand the code
This part of the tutorial explains the most significant parts of the simple
store locator app, to help you understand how to build a similar app.
Style map elements
Styling is an important aspect of any
Google Maps app. You can use styling to emphasize the information you want to
present while suppressing details that your users don't need, all while making
your map look more beautiful. The mapStyle constant contains the JSON style
declaration, which defines the styling options for the map. The style
declaration sets the colors to use for each map feature, and also specifies
whether various features will be visible on the map. The following example
shows the JSON style declaration:
index.html is a minimal HTML file that loads the
Maps JavaScript API and app.js script, then displays
the map in a div which is styled so that the map takes up the entire page
when it loads. When index.html loads for the first time, the first script
tag loads app.js, and the second script tag loads the
Maps JavaScript API, specifying the API key and the name
of the callback function to invoke once loading is complete:
The initMap() function contains all of the functionality for the store
locator app. This function runs as soon as all of the content for the site has
finished downloading. The map constant gets the name of the div in which to
display the map, and specifies initial zoom level, the coordinates at which to
center the map, and the style declaration to use.
When a user clicks a marker, the listener function gets the data for that
location, and features HTML to construct an InfoWindow
to display the location's information. This function uses the category
property to determine the appropriate logo to display, and displays a
StreetView image using the
lat/lng coordinates for the selected location.
map.data.addListener('click', event => {
let category = event.feature.getProperty('category');
let name = event.feature.getProperty('name');
let description = event.feature.getProperty('description');
let hours = event.feature.getProperty('hours');
let phone = event.feature.getProperty('phone');
let position = event.feature.getGeometry().get();
let content = `
<img style="float:left; width:200px; margin-top:30px" src="img/logo_${category}.png">
<div style="margin-left:220px; margin-bottom:20px;">
<h2>${name}</h2><p>${description}</p>
<p><b>Open:</b> ${hours}<br/><b>Phone:</b> ${phone}</p>
<p><img src="https://maps.googleapis.com/maps/api/streetview?size=350x120&location=${position.lat()},${position.lng()}&key=${apiKey}"></p>
</div>
`;
infoWindow.setContent(content);
infoWindow.setPosition(position);
infoWindow.setOptions({pixelOffset: new google.maps.Size(0, -30)});
infoWindow.open(map);
});
Next step
See NYC Subway Locator to learn how to expand upon this store
locator app, by adding a Golang backend to handle marker clustering and
server-side filtering.