Created
February 1, 2022 10:27
-
-
Save hhkaos/35f90fec2e5451621d321e75cab1aa9e to your computer and use it in GitHub Desktop.
GMaps Platform: Solution Library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Address Selection</title> | |
<style> | |
body { | |
margin: 0; | |
} | |
.sb-title { | |
position: relative; | |
top: -12px; | |
font-family: Roboto, sans-serif; | |
font-weight: 500; | |
} | |
.sb-title-icon { | |
position: relative; | |
top: -5px; | |
} | |
.card-container { | |
display: flex; | |
height: 500px; | |
width: 600px; | |
} | |
.panel { | |
background: white; | |
width: 300px; | |
padding: 20px; | |
display: flex; | |
flex-direction: column; | |
justify-content: space-around; | |
} | |
.half-input-container { | |
display: flex; | |
justify-content: space-between; | |
} | |
.half-input { | |
max-width: 120px; | |
} | |
.map { | |
width: 300px; | |
} | |
h2 { | |
margin: 0; | |
font-family: Roboto, sans-serif; | |
} | |
input { | |
height: 30px; | |
} | |
input { | |
border: 0; | |
border-bottom: 1px solid black; | |
font-size: 14px; | |
font-family: Roboto, sans-serif; | |
font-style: normal; | |
font-weight: normal; | |
} | |
input:focus::placeholder { | |
color: white; | |
} | |
.button-cta { | |
height: 40px; | |
width: 40%; | |
background: #3367d6; | |
color: white; | |
font-size: 15px; | |
text-transform: uppercase; | |
font-family: Roboto, sans-serif; | |
border: 0; | |
border-radius: 3px; | |
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.48); | |
cursor: pointer; | |
} | |
</style> | |
<script> | |
"use strict"; | |
function initMap() { | |
const componentForm = [ | |
'location', | |
'locality', | |
'administrative_area_level_1', | |
'country', | |
'postal_code', | |
]; | |
const map = new google.maps.Map(document.getElementById("map"), { | |
zoom: 11, | |
center: { lat: 37.4221, lng: -122.0841 }, | |
mapTypeControl: false, | |
fullscreenControl: true, | |
zoomControl: true, | |
streetViewControl: true | |
}); | |
const marker = new google.maps.Marker({map: map, draggable: false}); | |
const autocompleteInput = document.getElementById('location'); | |
const autocomplete = new google.maps.places.Autocomplete(autocompleteInput, { | |
fields: ["address_components", "geometry", "name"], | |
types: ["address"], | |
}); | |
autocomplete.addListener('place_changed', function () { | |
marker.setVisible(false); | |
const place = autocomplete.getPlace(); | |
if (!place.geometry) { | |
// User entered the name of a Place that was not suggested and | |
// pressed the Enter key, or the Place Details request failed. | |
window.alert('No details available for input: \'' + place.name + '\''); | |
return; | |
} | |
renderAddress(place); | |
fillInAddress(place); | |
}); | |
function fillInAddress(place) { // optional parameter | |
const addressNameFormat = { | |
'street_number': 'short_name', | |
'route': 'long_name', | |
'locality': 'long_name', | |
'administrative_area_level_1': 'short_name', | |
'country': 'long_name', | |
'postal_code': 'short_name', | |
}; | |
const getAddressComp = function (type) { | |
for (const component of place.address_components) { | |
if (component.types[0] === type) { | |
return component[addressNameFormat[type]]; | |
} | |
} | |
return ''; | |
}; | |
document.getElementById('location').value = getAddressComp('street_number') + ' ' | |
+ getAddressComp('route'); | |
for (const component of componentForm) { | |
// Location field is handled separately above as it has different logic. | |
if (component !== 'location') { | |
document.getElementById(component).value = getAddressComp(component); | |
} | |
} | |
} | |
function renderAddress(place) { | |
map.setCenter(place.geometry.location); | |
marker.setPosition(place.geometry.location); | |
marker.setVisible(true); | |
} | |
} | |
</script> | |
</head> | |
<body> | |
<div class="card-container"> | |
<div class="panel"> | |
<div> | |
<img class="sb-title-icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/location_pin/v5/24px.svg" alt=""> | |
<span class="sb-title">Address Selection</span> | |
</div> | |
<input type="text" placeholder="Address" id="location"/> | |
<input type="text" placeholder="Apt, Suite, etc (optional)"/> | |
<input type="text" placeholder="City" id="locality"/> | |
<div class="half-input-container"> | |
<input type="text" class="half-input" placeholder="State/Province" id="administrative_area_level_1"/> | |
<input type="text" class="half-input" placeholder="Zip/Postal code" id="postal_code"/> | |
</div> | |
<input type="text" placeholder="Country" id="country"/> | |
<button class="button-cta">Checkout</button> | |
</div> | |
<div class="map" id="map"></div> | |
</div> | |
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&libraries=places&callback=initMap&solution_channel=GMP_QB_addressselection_v1_cABC" async defer></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Locator</title> | |
<meta name="viewport" content="width=device-width,initial-scale=1"> | |
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/handlebars/4.7.7/handlebars.min.js"></script> | |
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"> | |
<style> | |
html, | |
body { | |
height: 100%; | |
margin: 0; | |
padding: 0; | |
} | |
#map-container { | |
width: 100%; | |
height: 100%; | |
position: relative; | |
font-family: "Roboto", sans-serif; | |
box-sizing: border-box; | |
} | |
#map-container button { | |
background: none; | |
color: inherit; | |
border: none; | |
padding: 0; | |
font: inherit; | |
font-size: inherit; | |
cursor: pointer; | |
} | |
#map { | |
position: absolute; | |
left: 22em; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
} | |
#locations-panel { | |
position: absolute; | |
left: 0; | |
width: 22em; | |
top: 0; | |
bottom: 0; | |
overflow-y: auto; | |
background: white; | |
padding: 0.5em; | |
box-sizing: border-box; | |
} | |
@media only screen and (max-width: 876px) { | |
#map { | |
left: 0; | |
bottom: 50%; | |
} | |
#locations-panel { | |
top: 50%; | |
right: 0; | |
width: unset; | |
} | |
} | |
#locations-panel-list > header { | |
padding: 1.4em 1.4em 0 1.4em; | |
} | |
#locations-panel-list h1.search-title { | |
font-size: 1em; | |
font-weight: 500; | |
margin: 0; | |
} | |
#locations-panel-list h1.search-title > img { | |
vertical-align: bottom; | |
margin-top: -1em; | |
} | |
#locations-panel-list .search-input { | |
width: 100%; | |
margin-top: 0.8em; | |
position: relative; | |
} | |
#locations-panel-list .search-input input { | |
width: 100%; | |
border: 1px solid rgba(0, 0, 0, 0.2); | |
border-radius: 0.3em; | |
height: 2.2em; | |
box-sizing: border-box; | |
padding: 0 2.5em 0 1em; | |
font-size: 1em; | |
} | |
#locations-panel-list .search-input-overlay { | |
position: absolute; | |
} | |
#locations-panel-list .search-input-overlay.search { | |
right: 2px; | |
top: 2px; | |
bottom: 2px; | |
width: 2.4em; | |
} | |
#locations-panel-list .search-input-overlay.search button { | |
width: 100%; | |
height: 100%; | |
border-radius: 0.2em; | |
color: black; | |
background: transparent; | |
} | |
#locations-panel-list .search-input-overlay.search .icon { | |
margin-top: 0.05em; | |
vertical-align: top; | |
} | |
#locations-panel-list .section-name { | |
font-weight: 500; | |
font-size: 0.9em; | |
margin: 1.8em 0 1em 1.5em; | |
} | |
#locations-panel-list .location-result { | |
position: relative; | |
padding: 0.8em 3.5em 0.8em 1.4em; | |
border-bottom: 1px solid rgba(0, 0, 0, 0.12); | |
cursor: pointer; | |
} | |
#locations-panel-list .location-result:first-of-type { | |
border-top: 1px solid rgba(0, 0, 0, 0.12); | |
} | |
#locations-panel-list .location-result:last-of-type { | |
border-bottom: none; | |
} | |
#locations-panel-list .location-result.selected { | |
outline: 2px solid #4285f4; | |
} | |
#locations-panel-list button.select-location { | |
margin-bottom: 0.6em; | |
text-align: left; | |
} | |
#locations-panel-list .location-result h2.name { | |
font-size: 1em; | |
font-weight: 500; | |
margin: 0; | |
} | |
#locations-panel-list .location-result .address { | |
font-size: 0.9em; | |
margin-bottom: 0.5em; | |
} | |
#locations-panel-list .location-result .distance { | |
position: absolute; | |
top: 0.9em; | |
right: 0; | |
text-align: center; | |
font-size: 0.9em; | |
width: 5em; | |
} | |
#location-results-list { | |
list-style-type: none; | |
margin: 0; | |
padding: 0; | |
} | |
</style> | |
<script> | |
'use strict'; | |
/** | |
* Defines an instance of the Locator+ solution, to be instantiated | |
* when the Maps library is loaded. | |
*/ | |
function LocatorPlus(configuration) { | |
const locator = this; | |
locator.locations = configuration.locations || []; | |
locator.capabilities = configuration.capabilities || {}; | |
const mapEl = document.getElementById('map'); | |
const panelEl = document.getElementById('locations-panel'); | |
locator.panelListEl = document.getElementById('locations-panel-list'); | |
const sectionNameEl = | |
document.getElementById('location-results-section-name'); | |
const resultsContainerEl = document.getElementById('location-results-list'); | |
const itemsTemplate = Handlebars.compile( | |
document.getElementById('locator-result-items-tmpl').innerHTML); | |
locator.searchLocation = null; | |
locator.searchLocationMarker = null; | |
locator.selectedLocationIdx = null; | |
locator.userCountry = null; | |
// Initialize the map ------------------------------------------------------- | |
locator.map = new google.maps.Map(mapEl, configuration.mapOptions); | |
// Store selection. | |
const selectResultItem = function(locationIdx, panToMarker, scrollToResult) { | |
locator.selectedLocationIdx = locationIdx; | |
for (let locationElem of resultsContainerEl.children) { | |
locationElem.classList.remove('selected'); | |
if (getResultIndex(locationElem) === locator.selectedLocationIdx) { | |
locationElem.classList.add('selected'); | |
if (scrollToResult) { | |
panelEl.scrollTop = locationElem.offsetTop; | |
} | |
} | |
} | |
if (panToMarker && (locationIdx != null)) { | |
locator.map.panTo(locator.locations[locationIdx].coords); | |
} | |
}; | |
// Create a marker for each location. | |
const markers = locator.locations.map(function(location, index) { | |
const marker = new google.maps.Marker({ | |
position: location.coords, | |
map: locator.map, | |
title: location.title, | |
}); | |
marker.addListener('click', function() { | |
selectResultItem(index, false, true); | |
}); | |
return marker; | |
}); | |
// Fit map to marker bounds. | |
locator.updateBounds = function() { | |
const bounds = new google.maps.LatLngBounds(); | |
if (locator.searchLocationMarker) { | |
bounds.extend(locator.searchLocationMarker.getPosition()); | |
} | |
for (let i = 0; i < markers.length; i++) { | |
bounds.extend(markers[i].getPosition()); | |
} | |
locator.map.fitBounds(bounds); | |
}; | |
if (locator.locations.length) { | |
locator.updateBounds(); | |
} | |
// Get the distance of a store location to the user's location, | |
// used in sorting the list. | |
const getLocationDistance = function(location) { | |
if (!locator.searchLocation) return null; | |
// Use travel distance if available (from Distance Matrix). | |
if (location.travelDistanceValue != null) { | |
return location.travelDistanceValue; | |
} | |
// Fall back to straight-line distance. | |
return google.maps.geometry.spherical.computeDistanceBetween( | |
new google.maps.LatLng(location.coords), | |
locator.searchLocation.location); | |
}; | |
// Render the results list -------------------------------------------------- | |
const getResultIndex = function(elem) { | |
return parseInt(elem.getAttribute('data-location-index')); | |
}; | |
locator.renderResultsList = function() { | |
let locations = locator.locations.slice(); | |
for (let i = 0; i < locations.length; i++) { | |
locations[i].index = i; | |
} | |
if (locator.searchLocation) { | |
sectionNameEl.textContent = | |
'Nearest locations (' + locations.length + ')'; | |
locations.sort(function(a, b) { | |
return getLocationDistance(a) - getLocationDistance(b); | |
}); | |
} else { | |
sectionNameEl.textContent = `All locations (${locations.length})`; | |
} | |
const resultItemContext = { locations: locations }; | |
resultsContainerEl.innerHTML = itemsTemplate(resultItemContext); | |
for (let item of resultsContainerEl.children) { | |
const resultIndex = getResultIndex(item); | |
if (resultIndex === locator.selectedLocationIdx) { | |
item.classList.add('selected'); | |
} | |
const resultSelectionHandler = function() { | |
selectResultItem(resultIndex, true, false); | |
}; | |
// Clicking anywhere on the item selects this location. | |
// Additionally, create a button element to make this behavior | |
// accessible under tab navigation. | |
item.addEventListener('click', resultSelectionHandler); | |
item.querySelector('.select-location') | |
.addEventListener('click', function(e) { | |
resultSelectionHandler(); | |
e.stopPropagation(); | |
}); | |
} | |
}; | |
// Optional capability initialization -------------------------------------- | |
initializeSearchInput(locator); | |
initializeDistanceMatrix(locator); | |
// Initial render of results ----------------------------------------------- | |
locator.renderResultsList(); | |
} | |
/** When the search input capability is enabled, initialize it. */ | |
function initializeSearchInput(locator) { | |
const geocodeCache = new Map(); | |
const geocoder = new google.maps.Geocoder(); | |
const searchInputEl = document.getElementById('location-search-input'); | |
const searchButtonEl = document.getElementById('location-search-button'); | |
const updateSearchLocation = function(address, location) { | |
if (locator.searchLocationMarker) { | |
locator.searchLocationMarker.setMap(null); | |
} | |
if (!location) { | |
locator.searchLocation = null; | |
return; | |
} | |
locator.searchLocation = {'address': address, 'location': location}; | |
locator.searchLocationMarker = new google.maps.Marker({ | |
position: location, | |
map: locator.map, | |
title: 'My location', | |
icon: { | |
path: google.maps.SymbolPath.CIRCLE, | |
scale: 12, | |
fillColor: '#3367D6', | |
fillOpacity: 0.5, | |
strokeOpacity: 0, | |
} | |
}); | |
// Update the locator's idea of the user's country, used for units. Use | |
// `formatted_address` instead of the more structured `address_components` | |
// to avoid an additional billed call. | |
const addressParts = address.split(' '); | |
locator.userCountry = addressParts[addressParts.length - 1]; | |
// Update map bounds to include the new location marker. | |
locator.updateBounds(); | |
// Update the result list so we can sort it by proximity. | |
locator.renderResultsList(); | |
locator.updateTravelTimes(); | |
}; | |
const geocodeSearch = function(query) { | |
if (!query) { | |
return; | |
} | |
const handleResult = function(geocodeResult) { | |
searchInputEl.value = geocodeResult.formatted_address; | |
updateSearchLocation( | |
geocodeResult.formatted_address, geocodeResult.geometry.location); | |
}; | |
if (geocodeCache.has(query)) { | |
handleResult(geocodeCache.get(query)); | |
return; | |
} | |
const request = {address: query, bounds: locator.map.getBounds()}; | |
geocoder.geocode(request, function(results, status) { | |
if (status === 'OK') { | |
if (results.length > 0) { | |
const result = results[0]; | |
geocodeCache.set(query, result); | |
handleResult(result); | |
} | |
} | |
}); | |
}; | |
// Set up geocoding on the search input. | |
searchButtonEl.addEventListener('click', function() { | |
geocodeSearch(searchInputEl.value.trim()); | |
}); | |
// Initialize Autocomplete. | |
initializeSearchInputAutocomplete( | |
locator, searchInputEl, geocodeSearch, updateSearchLocation); | |
} | |
/** Add Autocomplete to the search input. */ | |
function initializeSearchInputAutocomplete( | |
locator, searchInputEl, fallbackSearch, searchLocationUpdater) { | |
// Set up Autocomplete on the search input. Bias results to map viewport. | |
const autocomplete = new google.maps.places.Autocomplete(searchInputEl, { | |
types: ['geocode'], | |
fields: ['place_id', 'formatted_address', 'geometry.location'] | |
}); | |
autocomplete.bindTo('bounds', locator.map); | |
autocomplete.addListener('place_changed', function() { | |
const placeResult = autocomplete.getPlace(); | |
if (!placeResult.geometry) { | |
// Hitting 'Enter' without selecting a suggestion will result in a | |
// placeResult with only the text input value as the 'name' field. | |
fallbackSearch(placeResult.name); | |
return; | |
} | |
searchLocationUpdater( | |
placeResult.formatted_address, placeResult.geometry.location); | |
}); | |
} | |
/** Initialize Distance Matrix for the locator. */ | |
function initializeDistanceMatrix(locator) { | |
const distanceMatrixService = new google.maps.DistanceMatrixService(); | |
// Annotate travel times to the selected location using Distance Matrix. | |
locator.updateTravelTimes = function() { | |
if (!locator.searchLocation) return; | |
const units = (locator.userCountry === 'USA') ? | |
google.maps.UnitSystem.IMPERIAL : | |
google.maps.UnitSystem.METRIC; | |
const request = { | |
origins: [locator.searchLocation.location], | |
destinations: locator.locations.map(function(x) { | |
return x.coords; | |
}), | |
travelMode: google.maps.TravelMode.DRIVING, | |
unitSystem: units, | |
}; | |
const callback = function(response, status) { | |
if (status === 'OK') { | |
const distances = response.rows[0].elements; | |
for (let i = 0; i < distances.length; i++) { | |
const distResult = distances[i]; | |
let travelDistanceText, travelDistanceValue; | |
if (distResult.status === 'OK') { | |
travelDistanceText = distResult.distance.text; | |
travelDistanceValue = distResult.distance.value; | |
} | |
const location = locator.locations[i]; | |
location.travelDistanceText = travelDistanceText; | |
location.travelDistanceValue = travelDistanceValue; | |
} | |
// Re-render the results list, in case the ordering has changed. | |
locator.renderResultsList(); | |
} | |
}; | |
distanceMatrixService.getDistanceMatrix(request, callback); | |
}; | |
} | |
</script> | |
<script> | |
const CONFIGURATION = { | |
"locations": [ | |
{"title":"Death Valley National Park","address1":"California","address2":"United States","coords":{"lat":36.4617,"lng":-116.8668},"placeId":"ChIJR4qudndLx4ARVLDye3zwycw"}, | |
{"title":"Yosemite National Park","address1":"California","address2":"USA","coords":{"lat":37.7487,"lng":-119.5873},"placeId":"ChIJxeyK9Z3wloAR_gOA7SycJC0"}, | |
{"title":"Joshua Tree National Park","address1":"California","address2":"USA","coords":{"lat":33.9157,"lng":-115.8807},"placeId":"ChIJe6hluYWP2oAR4p3rOqftdxk"}, | |
{"title":"Lassen Volcanic National Park","address1":"California","address2":"USA","coords":{"lat":40.4377,"lng":-121.5338},"placeId":"ChIJvzhBwQdWnYARQmdmeqfYNI8"}, | |
{"title":"Sequoia National Park","address1":"California","address2":"USA","coords":{"lat":36.491,"lng":-118.8253},"placeId":"ChIJeWUZLX37v4ARZPQen_nfCkQ"}, | |
{"title":"Pinnacles National Park","address1":"California","address2":"USA","coords":{"lat":36.4938,"lng":-121.1465},"placeId":"ChIJn93OiYBDkoAR7kSomO77gps"}, | |
{"title":"Redwood National and State Parks","address1":"California","address2":"USA","coords":{"lat":41.3025,"lng":-124.0471},"placeId":"ChIJpX9B9TZm0FQRjl87lWYyzzY"}, | |
{"title":"Kings Canyon National Park","address1":"California","address2":"USA","coords":{"lat":36.74,"lng":-118.9633},"placeId":"ChIJe6fxX-7Vv4ARTA9DcLeDZII"} | |
], | |
"mapOptions": {"center":{"lat":38.0,"lng":-100.0},"fullscreenControl":true,"mapTypeControl":false,"streetViewControl":false,"zoom":4,"zoomControl":true,"maxZoom":17}, | |
"mapsApiKey": "YOUR_API_KEY_HERE" | |
}; | |
function initMap() { | |
new LocatorPlus(CONFIGURATION); | |
} | |
</script> | |
<script id="locator-result-items-tmpl" type="text/x-handlebars-template"> | |
{{#each locations}} | |
<li class="location-result" data-location-index="{{index}}"> | |
<button class="select-location"> | |
<h2 class="name">{{title}}</h2> | |
</button> | |
<div class="address">{{address1}}<br>{{address2}}</div> | |
{{#if travelDistanceText}} | |
<div class="distance">{{travelDistanceText}}</div> | |
{{/if}} | |
</li> | |
{{/each}} | |
</script> | |
</head> | |
<body> | |
<div id="map-container"> | |
<div id="locations-panel"> | |
<div id="locations-panel-list"> | |
<header> | |
<h1 class="search-title"> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/place/v15/24px.svg"/> | |
Find a location near you | |
</h1> | |
<div class="search-input"> | |
<input id="location-search-input" placeholder="Enter your address or zip code"> | |
<div id="search-overlay-search" class="search-input-overlay search"> | |
<button id="location-search-button"> | |
<img class="icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/search/v11/24px.svg" alt="Search"/> | |
</button> | |
</div> | |
</div> | |
</header> | |
<div class="section-name" id="location-results-section-name"> | |
All locations | |
</div> | |
<div class="results"> | |
<ul id="location-results-list"></ul> | |
</div> | |
</div> | |
</div> | |
<div id="map"></div> | |
</div> | |
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&callback=initMap&libraries=places,geometry&solution_channel=GMP_QB_locatorplus_v4_cABD" async defer></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<title>Neighborhood Discovery</title> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width,initial-scale=1"> | |
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/handlebars/4.7.7/handlebars.min.js"></script> | |
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"/> | |
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/> | |
<style> | |
html, body { | |
height: 100%; | |
margin: 0; | |
padding: 0; | |
} | |
.neighborhood-discovery { | |
box-sizing: border-box; | |
font-family: "Roboto", sans-serif; | |
height: 100%; | |
position: relative; | |
width: 100%; | |
} | |
.neighborhood-discovery a { | |
color: #4285f4; | |
text-decoration: none; | |
} | |
.neighborhood-discovery button { | |
background: none; | |
border: none; | |
color: inherit; | |
cursor: pointer; | |
font: inherit; | |
font-size: inherit; | |
padding: 0; | |
} | |
.neighborhood-discovery .info { | |
color: #555; | |
font-size: 0.9em; | |
margin-top: 0.3em; | |
} | |
.neighborhood-discovery .panel { | |
background: white; | |
bottom: 0; | |
box-sizing: border-box; | |
left: 0; | |
overflow-y: auto; | |
position: absolute; | |
top: 0; | |
width: 20em; | |
} | |
.neighborhood-discovery .panel.no-scroll { | |
overflow-y: hidden; | |
} | |
.neighborhood-discovery .photo { | |
background-color: #dadce0; | |
background-position: center; | |
background-size: cover; | |
border-radius: 0.3em; | |
cursor: pointer; | |
} | |
.neighborhood-discovery .navbar { | |
background: white; | |
position: sticky; | |
top: 0; | |
z-index: 2; | |
} | |
.neighborhood-discovery .right { | |
float: right; | |
} | |
.neighborhood-discovery .star-icon { | |
filter: invert(88%) sepia(60%) saturate(2073%) hue-rotate(318deg) brightness(93%) contrast(104%); | |
height: 1.2em; | |
margin-right: -0.3em; | |
margin-top: -0.08em; | |
vertical-align: top; | |
width: 1.2em; | |
} | |
.neighborhood-discovery .star-icon:last-child { | |
margin-right: 0.2em; | |
} | |
.neighborhood-discovery .map { | |
bottom: 0; | |
left: 20em; | |
position: absolute; | |
right: 0; | |
top: 0; | |
} | |
@media only screen and (max-width: 640px) { | |
.neighborhood-discovery .panel { | |
right: 0; | |
top: 50%; | |
width: unset; | |
} | |
.neighborhood-discovery .map { | |
bottom: 50%; | |
left: 0; | |
} | |
} | |
/* --------------------------- PLACES PANEL --------------------------- */ | |
.neighborhood-discovery .places-panel { | |
box-shadow: 0 0 10px rgb(60 64 67 / 28%); | |
z-index: 1; | |
} | |
.neighborhood-discovery .places-panel header { | |
padding: 0.5em; | |
} | |
.neighborhood-discovery .search-input input { | |
border: 1px solid rgba(0, 0, 0, 0.2); | |
border-radius: 0.3em; | |
box-sizing: border-box; | |
font-size: 1em; | |
height: 2.2em; | |
padding: 0 2.5em 0 1em; | |
width: 100%; | |
} | |
.neighborhood-discovery .search-input button { | |
position: absolute; | |
right: 0.8em; | |
top: 0.8em; | |
} | |
.neighborhood-discovery .show-more-button { | |
bottom: 0.5em; | |
display: none; | |
left: 28%; | |
line-height: 1.5em; | |
padding: 0.6em; | |
position: relative; | |
width: 44%; | |
} | |
.neighborhood-discovery .show-more-button.sticky { | |
background: white; | |
border-radius: 1.5em; | |
box-shadow: 0 4px 10px rgb(60 64 67 / 28%); | |
position: sticky; | |
z-index: 2; | |
} | |
.neighborhood-discovery .show-more-button:disabled { | |
opacity: 0.5; | |
} | |
.neighborhood-discovery .place-results-list { | |
list-style-type: none; | |
margin: 0; | |
padding: 0; | |
} | |
.neighborhood-discovery .place-result { | |
border-top: 1px solid rgba(0, 0, 0, 0.12); | |
cursor: pointer; | |
display: flex; | |
padding: 0.8em; | |
} | |
.neighborhood-discovery .place-result .text { | |
flex-grow: 1; | |
} | |
.neighborhood-discovery .place-result .name { | |
font-size: 1em; | |
font-weight: 500; | |
text-align: left; | |
} | |
.neighborhood-discovery .place-result .photo { | |
flex: 0 0 4em; | |
height: 4em; | |
margin-left: 0.8em; | |
} | |
/* -------------------------- DETAILS PANEL --------------------------- */ | |
.neighborhood-discovery .details-panel { | |
display: none; | |
z-index: 20; | |
} | |
.neighborhood-discovery .details-panel .back-button { | |
color: #4285f4; | |
padding: 0.9em; | |
} | |
.neighborhood-discovery .details-panel .back-button .icon { | |
/* Match link color #4285f4 */ | |
filter: invert(47%) sepia(71%) saturate(2372%) hue-rotate(200deg) brightness(97%) contrast(98%); | |
height: 1.2em; | |
width: 1.2em; | |
vertical-align: bottom; | |
} | |
.neighborhood-discovery .details-panel header { | |
padding: 0.9em; | |
} | |
.neighborhood-discovery .details-panel h2 { | |
font-size: 1.4em; | |
font-weight: 400; | |
margin: 0; | |
} | |
.neighborhood-discovery .details-panel .section { | |
border-top: 1px solid rgba(0, 0, 0, 0.12); | |
padding: 0.9em; | |
} | |
.neighborhood-discovery .details-panel .contact { | |
align-items: center; | |
display: flex; | |
font-size: 0.9em; | |
margin: 0.8em 0; | |
} | |
.neighborhood-discovery .details-panel .contact .icon { | |
width: 1.5em; | |
height: 1.5em; | |
} | |
.neighborhood-discovery .details-panel .contact .text { | |
margin-left: 1em; | |
} | |
.neighborhood-discovery .details-panel .contact .weekday { | |
display: inline-block; | |
width: 5em; | |
} | |
.neighborhood-discovery .details-panel .photos { | |
text-align: center; | |
} | |
.neighborhood-discovery .details-panel .photo { | |
display: inline-block; | |
height: 5.5em; | |
width: 5.5em; | |
} | |
.neighborhood-discovery .details-panel .review { | |
margin-top: 1.2em; | |
} | |
.neighborhood-discovery .details-panel .review .reviewer-avatar { | |
background-repeat: no-repeat; | |
background-size: cover; | |
float: left; | |
height: 1.8em; | |
margin-right: 0.8em; | |
width: 1.8em; | |
} | |
.neighborhood-discovery .details-panel .review .reviewer-name { | |
color: #202124; | |
font-weight: 500; | |
line-height: 1.8em; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
.neighborhood-discovery .details-panel .review .rating { | |
margin: 0.5em 0; | |
} | |
.neighborhood-discovery .details-panel .attribution { | |
color: #777; | |
margin: 0; | |
font-size: 0.8em; | |
font-style: italic; | |
} | |
/* --------------------------- PHOTO MODAL ---------------------------- */ | |
.neighborhood-discovery .photo-modal { | |
background: rgba(0, 0, 0, 0.8); | |
display: none; | |
height: 100%; | |
position: fixed; | |
width: 100%; | |
z-index: 100; | |
} | |
.neighborhood-discovery .photo-modal > img { | |
bottom: 0; | |
left: 0; | |
margin: auto; | |
max-height: 100%; | |
max-width: 100%; | |
position: absolute; | |
right: 0; | |
top: 0; | |
} | |
.neighborhood-discovery .photo-modal > div { | |
border-radius: 0.4em; | |
color: white; | |
background: rgba(0, 0, 0, 0.6); | |
margin: 1em; | |
padding: 0.9em; | |
position: absolute; | |
} | |
.neighborhood-discovery .photo-modal .back-button .icon { | |
filter: brightness(0) invert(1); | |
margin: 0.4em 0.6em 0 0; | |
} | |
.neighborhood-discovery .photo-modal .photo-text { | |
float: right; | |
} | |
.neighborhood-discovery .photo-modal .photo-attrs { | |
font-size: 0.8em; | |
margin-top: 0.3em; | |
} | |
</style> | |
<script> | |
'use strict'; | |
/** Hides a DOM element and optionally focuses on focusEl. */ | |
function hideElement(el, focusEl) { | |
el.style.display = 'none'; | |
if (focusEl) focusEl.focus(); | |
} | |
/** Shows a DOM element that has been hidden and optionally focuses on focusEl. */ | |
function showElement(el, focusEl) { | |
el.style.display = 'block'; | |
if (focusEl) focusEl.focus(); | |
} | |
/** Determines if a DOM element contains content that cannot be scrolled into view. */ | |
function hasHiddenContent(el) { | |
const noscroll = window.getComputedStyle(el).overflowY.includes('hidden'); | |
return noscroll && el.scrollHeight > el.clientHeight; | |
} | |
/** Format a Place Type string by capitalizing and replacing underscores with spaces. */ | |
function formatPlaceType(str) { | |
const capitalized = str.charAt(0).toUpperCase() + str.slice(1); | |
return capitalized.replace(/_/g, ' '); | |
} | |
/** Initializes an array of zeros with the given size. */ | |
function initArray(arraySize) { | |
const array = []; | |
while (array.length < arraySize) { | |
array.push(0); | |
} | |
return array; | |
} | |
/** Assigns star icons to an object given its rating (out of 5). */ | |
function addStarIcons(obj) { | |
if (!obj.rating) return; | |
const starsOutOfTen = Math.round(2 * obj.rating); | |
const fullStars = Math.floor(starsOutOfTen / 2); | |
const halfStars = fullStars !== starsOutOfTen / 2 ? 1 : 0; | |
const emptyStars = 5 - fullStars - halfStars; | |
// Express stars as arrays to make iterating in Handlebars easy. | |
obj.fullStarIcons = initArray(fullStars); | |
obj.halfStarIcons = initArray(halfStars); | |
obj.emptyStarIcons = initArray(emptyStars); | |
} | |
/** | |
* Constructs an array of opening hours by day from a PlaceOpeningHours object, | |
* where adjacent days of week with the same hours are collapsed into one element. | |
*/ | |
function parseDaysHours(openingHours) { | |
const daysHours = openingHours.weekday_text.map((e) => e.split(/\:\s+/)) | |
.map((e) => ({'days': e[0].substr(0, 3), 'hours': e[1]})); | |
for (let i = 1; i < daysHours.length; i++) { | |
if (daysHours[i - 1].hours === daysHours[i].hours) { | |
if (daysHours[i - 1].days.indexOf('-') !== -1) { | |
daysHours[i - 1].days = | |
daysHours[i - 1].days.replace(/\w+$/, daysHours[i].days); | |
} else { | |
daysHours[i - 1].days += ' - ' + daysHours[i].days; | |
} | |
daysHours.splice(i--, 1); | |
} | |
} | |
return daysHours; | |
} | |
/** Number of POIs to show on widget load. */ | |
const ND_NUM_PLACES_INITIAL = 5; | |
/** Number of additional POIs to show when 'Show More' button is clicked. */ | |
const ND_NUM_PLACES_SHOW_MORE = 5; | |
/** Maximum number of place photos to show on the details panel. */ | |
const ND_NUM_PLACE_PHOTOS_MAX = 6; | |
/** Minimum zoom level at which the default map POI pins will be shown. */ | |
const ND_DEFAULT_POI_MIN_ZOOM = 18; | |
/** Mapping of Place Types to Material Icons used to render custom map markers. */ | |
const ND_MARKER_ICONS_BY_TYPE = { | |
// Full list of icons can be found at https://fonts.google.com/icons | |
'_default': 'circle', | |
'restaurant': 'restaurant', | |
'cafe': 'local_cafe', | |
'bar': 'local_bar', | |
'park': 'park', | |
'museum': 'museum', | |
'supermarket': 'local_grocery_store', | |
'clothing_store': 'local_mall', | |
'department_store': 'local_mall', | |
'shopping_mall': 'local_mall', | |
'primary_school': 'school', | |
'secondary_school': 'school', | |
}; | |
/** | |
* Defines an instance of the Neighborhood Discovery widget, to be | |
* instantiated when the Maps library is loaded. | |
*/ | |
function NeighborhoodDiscovery(configuration) { | |
const widget = this; | |
const widgetEl = document.querySelector('.neighborhood-discovery'); | |
widget.center = configuration.mapOptions.center; | |
widget.places = configuration.pois || []; | |
// Initialize core functionalities ------------------------------------- | |
initializeMap(); | |
initializePlaceDetails(); | |
initializeSidePanel(); | |
// Initialize additional capabilities ---------------------------------- | |
initializeSearchInput(); | |
// Initializer function definitions ------------------------------------ | |
/** Initializes the interactive map and adds place markers. */ | |
function initializeMap() { | |
const mapOptions = configuration.mapOptions; | |
widget.mapBounds = new google.maps.Circle( | |
{center: widget.center, radius: configuration.mapRadius}).getBounds(); | |
mapOptions.restriction = {latLngBounds: widget.mapBounds}; | |
mapOptions.mapTypeControlOptions = {position: google.maps.ControlPosition.TOP_RIGHT}; | |
widget.map = new google.maps.Map(widgetEl.querySelector('.map'), mapOptions); | |
widget.map.fitBounds(widget.mapBounds, /* padding= */ 0); | |
widget.map.addListener('click', (e) => { | |
// Check if user clicks on a POI pin from the base map. | |
if (e.placeId) { | |
e.stop(); | |
widget.selectPlaceById(e.placeId); | |
} | |
}); | |
widget.map.addListener('zoom_changed', () => { | |
// Customize map styling to show/hide default POI pins or text based on zoom level. | |
const hideDefaultPoiPins = widget.map.getZoom() < ND_DEFAULT_POI_MIN_ZOOM; | |
widget.map.setOptions({ | |
styles: [{ | |
featureType: 'poi', | |
elementType: hideDefaultPoiPins ? 'labels' : 'labels.text', | |
stylers: [{visibility: 'off'}], | |
}], | |
}); | |
}); | |
const markerPath = widgetEl.querySelector('.marker-pin path').getAttribute('d'); | |
const drawMarker = function(title, position, fillColor, strokeColor, labelText) { | |
return new google.maps.Marker({ | |
title: title, | |
position: position, | |
map: widget.map, | |
icon: { | |
path: markerPath, | |
fillColor: fillColor, | |
fillOpacity: 1, | |
strokeColor: strokeColor, | |
anchor: new google.maps.Point(13, 35), | |
labelOrigin: new google.maps.Point(13, 13), | |
}, | |
label: { | |
text: labelText, | |
color: 'white', | |
fontSize: '16px', | |
fontFamily: 'Material Icons', | |
}, | |
}); | |
}; | |
// Add marker for the specified Place object. | |
widget.addPlaceMarker = function(place) { | |
place.marker = drawMarker(place.name, place.coords, '#EA4335', '#C5221F', place.icon); | |
place.marker.addListener('click', () => void widget.selectPlaceById(place.placeId)); | |
}; | |
// Fit map to bounds that contain all markers of the specified Place objects. | |
widget.updateBounds = function(places) { | |
const bounds = new google.maps.LatLngBounds(); | |
bounds.extend(widget.center); | |
for (let place of places) { | |
bounds.extend(place.marker.getPosition()); | |
} | |
widget.map.fitBounds(bounds, /* padding= */ 100); | |
}; | |
// Marker used to highlight a place from Autocomplete search. | |
widget.selectedPlaceMarker = new google.maps.Marker({title: 'Point of Interest'}); | |
} | |
/** Initializes Place Details service for the widget. */ | |
function initializePlaceDetails() { | |
const detailsService = new google.maps.places.PlacesService(widget.map); | |
const placeIdsToDetails = new Map(); // Create object to hold Place results. | |
for (let place of widget.places) { | |
placeIdsToDetails.set(place.placeId, place); | |
place.fetchedFields = new Set(['place_id']); | |
} | |
widget.fetchPlaceDetails = function(placeId, fields, callback) { | |
if (!placeId || !fields) return; | |
// Check for field existence in Place object. | |
let place = placeIdsToDetails.get(placeId); | |
if (!place) { | |
place = {placeId: placeId, fetchedFields: new Set(['place_id'])}; | |
placeIdsToDetails.set(placeId, place); | |
} | |
const missingFields = fields.filter((field) => !place.fetchedFields.has(field)); | |
if (missingFields.length === 0) { | |
callback(place); | |
return; | |
} | |
const request = {placeId: placeId, fields: missingFields}; | |
let retryCount = 0; | |
const processResult = function(result, status) { | |
if (status !== google.maps.places.PlacesServiceStatus.OK) { | |
// If query limit has been reached, wait before making another call; | |
// Increase wait time of each successive retry with exponential backoff | |
// and terminate after five failed attempts. | |
if (status === google.maps.places.PlacesServiceStatus.OVER_QUERY_LIMIT && | |
retryCount < 5) { | |
const delay = (Math.pow(2, retryCount) + Math.random()) * 500; | |
setTimeout(() => void detailsService.getDetails(request, processResult), delay); | |
retryCount++; | |
} | |
return; | |
} | |
// Basic details. | |
if (result.name) place.name = result.name; | |
if (result.geometry) place.coords = result.geometry.location; | |
if (result.formatted_address) place.address = result.formatted_address; | |
if (result.photos) { | |
place.photos = result.photos.map((photo) => ({ | |
urlSmall: photo.getUrl({maxWidth: 200, maxHeight: 200}), | |
urlLarge: photo.getUrl({maxWidth: 1200, maxHeight: 1200}), | |
attrs: photo.html_attributions, | |
})).slice(0, ND_NUM_PLACE_PHOTOS_MAX); | |
} | |
if (result.types) { | |
place.type = formatPlaceType(result.types[0]); | |
place.icon = ND_MARKER_ICONS_BY_TYPE['_default']; | |
for (let type of result.types) { | |
if (type in ND_MARKER_ICONS_BY_TYPE) { | |
place.type = formatPlaceType(type); | |
place.icon = ND_MARKER_ICONS_BY_TYPE[type]; | |
break; | |
} | |
} | |
} | |
if (result.url) place.url = result.url; | |
// Contact details. | |
if (result.website) { | |
place.website = result.website; | |
const url = new URL(place.website); | |
place.websiteDomain = url.hostname; | |
} | |
if (result.formatted_phone_number) place.phoneNumber = result.formatted_phone_number; | |
if (result.opening_hours) place.openingHours = parseDaysHours(result.opening_hours); | |
// Review details. | |
if (result.rating) { | |
place.rating = result.rating; | |
addStarIcons(place); | |
} | |
if (result.user_ratings_total) place.numReviews = result.user_ratings_total; | |
if (result.price_level) { | |
place.priceLevel = result.price_level; | |
place.dollarSigns = initArray(result.price_level); | |
} | |
if (result.reviews) { | |
place.reviews = result.reviews; | |
for (let review of place.reviews) { | |
addStarIcons(review); | |
} | |
} | |
for (let field of missingFields) { | |
place.fetchedFields.add(field); | |
} | |
callback(place); | |
}; | |
// Use result from Place Autocomplete if available. | |
if (widget.placeIdsToAutocompleteResults) { | |
const autoCompleteResult = widget.placeIdsToAutocompleteResults.get(placeId); | |
if (autoCompleteResult) { | |
processResult(autoCompleteResult, google.maps.places.PlacesServiceStatus.OK); | |
return; | |
} | |
} | |
detailsService.getDetails(request, processResult); | |
}; | |
} | |
/** Initializes the side panel that holds curated POI results. */ | |
function initializeSidePanel() { | |
const placesPanelEl = widgetEl.querySelector('.places-panel'); | |
const detailsPanelEl = widgetEl.querySelector('.details-panel'); | |
const placeResultsEl = widgetEl.querySelector('.place-results-list'); | |
const showMoreButtonEl = widgetEl.querySelector('.show-more-button'); | |
const photoModalEl = widgetEl.querySelector('.photo-modal'); | |
const detailsTemplate = Handlebars.compile( | |
document.getElementById('nd-place-details-tmpl').innerHTML); | |
const resultsTemplate = Handlebars.compile( | |
document.getElementById('nd-place-results-tmpl').innerHTML); | |
// Show specified POI photo in a modal. | |
const showPhotoModal = function(photo, placeName) { | |
const prevFocusEl = document.activeElement; | |
const imgEl = photoModalEl.querySelector('img'); | |
imgEl.src = photo.urlLarge; | |
const backButtonEl = photoModalEl.querySelector('.back-button'); | |
backButtonEl.addEventListener('click', () => { | |
hideElement(photoModalEl, prevFocusEl); | |
imgEl.src = ''; | |
}); | |
photoModalEl.querySelector('.photo-place').innerHTML = placeName; | |
photoModalEl.querySelector('.photo-attrs span').innerHTML = photo.attrs; | |
const attributionEl = photoModalEl.querySelector('.photo-attrs a'); | |
if (attributionEl) attributionEl.setAttribute('target', '_blank'); | |
photoModalEl.addEventListener('click', (e) => { | |
if (e.target === photoModalEl) { | |
hideElement(photoModalEl, prevFocusEl); | |
imgEl.src = ''; | |
} | |
}); | |
showElement(photoModalEl, backButtonEl); | |
}; | |
// Select a place by id and show details. | |
let selectedPlaceId; | |
widget.selectPlaceById = function(placeId, panToMarker) { | |
if (selectedPlaceId === placeId) return; | |
selectedPlaceId = placeId; | |
const prevFocusEl = document.activeElement; | |
const showDetailsPanel = function(place) { | |
detailsPanelEl.innerHTML = detailsTemplate(place); | |
const backButtonEl = detailsPanelEl.querySelector('.back-button'); | |
backButtonEl.addEventListener('click', () => { | |
hideElement(detailsPanelEl, prevFocusEl); | |
selectedPlaceId = undefined; | |
widget.selectedPlaceMarker.setMap(null); | |
}); | |
detailsPanelEl.querySelectorAll('.photo').forEach((photoEl, i) => { | |
photoEl.addEventListener('click', () => { | |
showPhotoModal(place.photos[i], place.name); | |
}); | |
}); | |
showElement(detailsPanelEl, backButtonEl); | |
detailsPanelEl.scrollTop = 0; | |
}; | |
const processResult = function(place) { | |
if (place.marker) { | |
widget.selectedPlaceMarker.setMap(null); | |
} else { | |
widget.selectedPlaceMarker.setPosition(place.coords); | |
widget.selectedPlaceMarker.setMap(widget.map); | |
} | |
if (panToMarker) { | |
widget.map.panTo(place.coords); | |
} | |
showDetailsPanel(place); | |
}; | |
widget.fetchPlaceDetails(placeId, [ | |
'name', 'types', 'geometry.location', 'formatted_address', 'photo', 'url', | |
'website', 'formatted_phone_number', 'opening_hours', | |
'rating', 'user_ratings_total', 'price_level', 'review', | |
], processResult); | |
}; | |
// Render the specified place objects and append them to the POI list. | |
const renderPlaceResults = function(places, startIndex) { | |
placeResultsEl.insertAdjacentHTML('beforeend', resultsTemplate({places: places})); | |
placeResultsEl.querySelectorAll('.place-result').forEach((resultEl, i) => { | |
const place = places[i - startIndex]; | |
if (!place) return; | |
// Clicking anywhere on the item selects the place. | |
// Additionally, create a button element to make this behavior | |
// accessible under tab navigation. | |
resultEl.addEventListener('click', () => { | |
widget.selectPlaceById(place.placeId, /* panToMarker= */ true); | |
}); | |
resultEl.querySelector('.name').addEventListener('click', (e) => { | |
widget.selectPlaceById(place.placeId, /* panToMarker= */ true); | |
e.stopPropagation(); | |
}); | |
resultEl.querySelector('.photo').addEventListener('click', (e) => { | |
showPhotoModal(place.photos[0], place.name); | |
e.stopPropagation(); | |
}); | |
widget.addPlaceMarker(place); | |
}); | |
}; | |
// Index of next Place object to show in the POI list. | |
let nextPlaceIndex = 0; | |
// Fetch and show basic info for the next N places. | |
const showNextPlaces = function(n) { | |
const nextPlaces = widget.places.slice(nextPlaceIndex, nextPlaceIndex + n); | |
if (nextPlaces.length < 1) { | |
hideElement(showMoreButtonEl); | |
return; | |
} | |
showMoreButtonEl.disabled = true; | |
// Keep track of the number of Places calls that have not finished. | |
let count = nextPlaces.length; | |
for (let place of nextPlaces) { | |
const processResult = function(place) { | |
count--; | |
if (count > 0) return; | |
renderPlaceResults(nextPlaces, nextPlaceIndex); | |
nextPlaceIndex += n; | |
widget.updateBounds(widget.places.slice(0, nextPlaceIndex)); | |
const hasMorePlacesToShow = nextPlaceIndex < widget.places.length; | |
if (hasMorePlacesToShow || hasHiddenContent(placesPanelEl)) { | |
showElement(showMoreButtonEl); | |
showMoreButtonEl.disabled = false; | |
} else { | |
hideElement(showMoreButtonEl); | |
} | |
}; | |
widget.fetchPlaceDetails(place.placeId, [ | |
'name', 'types', 'geometry.location', | |
'photo', | |
'rating', 'user_ratings_total', 'price_level', | |
], processResult); | |
} | |
}; | |
showNextPlaces(ND_NUM_PLACES_INITIAL); | |
showMoreButtonEl.addEventListener('click', () => { | |
placesPanelEl.classList.remove('no-scroll'); | |
showMoreButtonEl.classList.remove('sticky'); | |
showNextPlaces(ND_NUM_PLACES_SHOW_MORE); | |
}); | |
} | |
/** Initializes Search Input for the widget. */ | |
function initializeSearchInput() { | |
const searchInputEl = widgetEl.querySelector('.place-search-input'); | |
widget.placeIdsToAutocompleteResults = new Map(); | |
// Set up Autocomplete on the search input. | |
const autocomplete = new google.maps.places.Autocomplete(searchInputEl, { | |
types: ['establishment'], | |
fields: [ | |
'place_id', 'name', 'types', 'geometry.location', 'formatted_address', 'photo', 'url', | |
'website', 'formatted_phone_number', 'opening_hours', | |
'rating', 'user_ratings_total', 'price_level', 'review', | |
], | |
bounds: widget.mapBounds, | |
strictBounds: true, | |
}); | |
autocomplete.addListener('place_changed', () => { | |
const place = autocomplete.getPlace(); | |
widget.placeIdsToAutocompleteResults.set(place.place_id, place); | |
widget.selectPlaceById(place.place_id, /* panToMarker= */ true); | |
searchInputEl.value = ''; | |
}); | |
} | |
} | |
</script> | |
<script> | |
const CONFIGURATION = { | |
"pois": [ | |
{"placeId": "ChIJO7l_l7f8cQ0Rf6IhEu_RjYA"}, | |
{"placeId": "ChIJtRFSAVrjcQ0Rjs7Ibtt2vtY"}, | |
{"placeId": "ChIJ45mdhb78cQ0R3qfxRyTkzgI"}, | |
{"placeId": "ChIJ0cmHHpH8cQ0RbpTJjlVri2k"}, | |
{"placeId": "ChIJ-TyL8778cQ0RGwyNKqccDOA"}, | |
{"placeId": "ChIJrYQTtPL8cQ0RU_vmc_NqB8A"}, | |
{"placeId": "ChIJLTqlF7_8cQ0RHosavBRI48Q"}, | |
{"placeId": "ChIJj6h_ML78cQ0RIFvCcFCLHRU"}, | |
{"placeId": "ChIJeWZxaLv8cQ0RlNvp2-m1x28"}, | |
{"placeId": "ChIJn2VauvL8cQ0R6Y5EcRRXztM"}, | |
{"placeId": "ChIJoZxr8r78cQ0RQuqduxRbyRM"}, | |
{"placeId": "ChIJ79TXfZX8cQ0RoeuqRI2yyno"}, | |
{"placeId": "ChIJjQaYi5b8cQ0RPOQ7xwds524"}, | |
{"placeId": "ChIJD0NYA-v8cQ0RWIwK74oOAOQ"}, | |
{"placeId": "ChIJY1N6W578cQ0RlUdw_5yVKic"}, | |
{"placeId": "ChIJjw4K1Jn8cQ0RET2g24bTW5c"}, | |
{"placeId": "ChIJ6V0eyOn8cQ0R2mbceLx1lUU"}, | |
{"placeId": "ChIJhTsy4738cQ0Rnw06gpIVjOU"}, | |
{"placeId": "ChIJ3dNkAb78cQ0Rrx1OLzKumxY"}, | |
{"placeId": "ChIJr3rLtsn8cQ0RUAoENt6qyqA"}, | |
{"placeId": "ChIJMaFlqK_8cQ0RrIHsW7KXvT8"} | |
], | |
"mapRadius": 3000, | |
"mapOptions": {"center":{"lat":37.1773363,"lng":-3.5985571},"fullscreenControl":true,"mapTypeControl":true,"streetViewControl":false,"zoom":16,"zoomControl":true,"maxZoom":20}, | |
"mapsApiKey": "YOUR_API_KEY_HERE" | |
}; | |
function initMap() { | |
new NeighborhoodDiscovery(CONFIGURATION); | |
} | |
</script> | |
<script id="nd-place-results-tmpl" type="text/x-handlebars-template"> | |
{{#each places}} | |
<li class="place-result"> | |
<div class="text"> | |
<button class="name">{{name}}</button> | |
<div class="info"> | |
{{#if rating}} | |
<span>{{rating}}</span> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star/v15/24px.svg" alt="rating" class="star-icon"/> | |
{{/if}} | |
{{#if numReviews}} | |
<span> ({{numReviews}})</span> | |
{{/if}} | |
{{#if priceLevel}} | |
· <span>{{#each dollarSigns}}${{/each}} </span> | |
{{/if}} | |
</div> | |
<div class="info">{{type}}</div> | |
</div> | |
<button class="photo" style="background-image:url({{photos.0.urlSmall}})" aria-label="show photo in viewer"></button> | |
</li> | |
{{/each}} | |
</script> | |
<script id="nd-place-details-tmpl" type="text/x-handlebars-template"> | |
<div class="navbar"> | |
<button class="back-button"> | |
<img class="icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/arrow_back/v11/24px.svg" alt="back"/> | |
Back | |
</button> | |
</div> | |
<header> | |
<h2>{{name}}</h2> | |
<div class="info"> | |
{{#if rating}} | |
<span class="star-rating-numeric">{{rating}}</span> | |
<span> | |
{{#each fullStarIcons}} | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star/v15/24px.svg" alt="full star" class="star-icon"/> | |
{{/each}} | |
{{#each halfStarIcons}} | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_half/v17/24px.svg" alt="half star" class="star-icon"/> | |
{{/each}} | |
{{#each emptyStarIcons}} | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_outline/v9/24px.svg" alt="empty star" class="star-icon"/> | |
{{/each}} | |
</span> | |
{{/if}} | |
{{#if numReviews}} | |
<a href="{{url}}" target="_blank">{{numReviews}} reviews</a> | |
{{else}} | |
<a href="{{url}}" target="_blank">See on Google Maps</a> | |
{{/if}} | |
{{#if priceLevel}} | |
· | |
<span class="price-dollars"> | |
{{#each dollarSigns}}${{/each}} | |
</span> | |
{{/if}} | |
</div> | |
{{#if type}} | |
<div class="info">{{type}}</div> | |
{{/if}} | |
</header> | |
<div class="section"> | |
{{#if address}} | |
<div class="contact"> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/place/v10/24px.svg" alt="Address" class="icon"/> | |
<div class="text"> | |
{{address}} | |
</div> | |
</div> | |
{{/if}} | |
{{#if website}} | |
<div class="contact"> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/public/v10/24px.svg" alt="Website" class="icon"/> | |
<div class="text"> | |
<a href="{{website}}" target="_blank">{{websiteDomain}}</a> | |
</div> | |
</div> | |
{{/if}} | |
{{#if phoneNumber}} | |
<div class="contact"> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/phone/v10/24px.svg" alt="Phone number" class="icon"/> | |
<div class="text"> | |
{{phoneNumber}} | |
</div> | |
</div> | |
{{/if}} | |
{{#if openingHours}} | |
<div class="contact"> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/schedule/v12/24px.svg" alt="Opening hours" class="icon"/> | |
<div class="text"> | |
{{#each openingHours}} | |
<div> | |
<span class="weekday">{{days}}</span> | |
<span class="hours">{{hours}}</span> | |
</div> | |
{{/each}} | |
</div> | |
</div> | |
{{/if}} | |
</div> | |
{{#if photos}} | |
<div class="photos section"> | |
{{#each photos}} | |
<button class="photo" style="background-image:url({{urlSmall}})" aria-label="show photo in viewer"></button> | |
{{/each}} | |
</div> | |
{{/if}} | |
{{#if reviews}} | |
<div class="reviews section"> | |
<p class="attribution">Reviews by Google users</p> | |
{{#each reviews}} | |
<div class="review"> | |
<a class="reviewer-identity" href="{{author_url}}" target="_blank"> | |
<div class="reviewer-avatar" style="background-image:url({{profile_photo_url}})"></div> | |
<div class="reviewer-name">{{author_name}}</div> | |
</a> | |
<div class="rating info"> | |
<span> | |
{{#each fullStarIcons}} | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star/v15/24px.svg" alt="full star" class="star-icon"/> | |
{{/each}} | |
{{#each halfStarIcons}} | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_half/v17/24px.svg" alt="half star" class="star-icon"/> | |
{{/each}} | |
{{#each emptyStarIcons}} | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/star_outline/v9/24px.svg" alt="empty star" class="star-icon"/> | |
{{/each}} | |
</span> | |
<span class="review-time">{{relative_time_description}}</span> | |
</div> | |
<div class="info">{{text}}</div> | |
</div> | |
{{/each}} | |
</div> | |
{{/if}} | |
{{#if html_attributions}} | |
<div class="section"> | |
{{#each html_attributions}} | |
<p class="attribution">{{{this}}}</p> | |
{{/each}} | |
</div> | |
{{/if}} | |
</script> | |
</head> | |
<body> | |
<div class="neighborhood-discovery"> | |
<div class="places-panel panel no-scroll"> | |
<header class="navbar"> | |
<div class="search-input"> | |
<input class="place-search-input" placeholder="Search nearby places"> | |
<button class="place-search-button"> | |
<img src="https://fonts.gstatic.com/s/i/googlematerialicons/search/v11/24px.svg" alt="search"/> | |
</button> | |
</div> | |
</header> | |
<div class="results"> | |
<ul class="place-results-list"></ul> | |
</div> | |
<button class="show-more-button sticky"> | |
<span>Show More</span> | |
<img class="right" src="https://fonts.gstatic.com/s/i/googlematerialicons/expand_more/v11/24px.svg" alt="expand"/> | |
</button> | |
</div> | |
<div class="details-panel panel"></div> | |
<div class="map"></div> | |
<div class="photo-modal"> | |
<img alt="place photo"/> | |
<div> | |
<button class="back-button"> | |
<img class="icon" src="https://fonts.gstatic.com/s/i/googlematerialicons/arrow_back/v11/24px.svg" alt="back"/> | |
</button> | |
<div class="photo-text"> | |
<div class="photo-place"></div> | |
<div class="photo-attrs">Photo by <span></span></div> | |
</div> | |
</div> | |
</div> | |
<svg class="marker-pin" xmlns="http://www.w3.org/2000/svg" width="26" height="38" fill="none"> | |
<path d="M13 0C5.817 0 0 5.93 0 13.267c0 7.862 5.59 10.81 9.555 17.624C12.09 35.248 11.342 38 13 38c1.723 0 .975-2.817 3.445-7.043C20.085 24.503 26 21.162 26 13.267 26 5.93 20.183 0 13 0Z"/> | |
</svg> | |
</div> | |
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&callback=initMap&libraries=places,geometry&solution_channel=GMP_QB_neighborhooddiscovery_v1_cADEF" async defer></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment