diff --git a/_config/config.yml b/_config/config.yml
index 33aafb9..b668eae 100644
--- a/_config/config.yml
+++ b/_config/config.yml
@@ -1,3 +1,8 @@
+---
+Name: elemental-locations
+After:
+ - '*'
+---
Dynamic\Locations\Model\Location:
extensions:
- Dynamic\Elements\Locations\Extension\LocationDataExtension
diff --git a/dist/css/map.css b/dist/css/map.css
index 23a7502..1d9801a 100644
--- a/dist/css/map.css
+++ b/dist/css/map.css
@@ -1,9 +1,20 @@
-#map {
+.locations-map {
height: 700px;
}
-#panel {
+/* Styling for an info pane that slides out from the left.
+* Hidden by default. */
+.locations-panel {
+ display: none;
+ position: absolute;
+ left: 0;
+ top: 0;
+ z-index: 1000;
+ background-color: #fff;
+ width: 300px;
height: 700px;
+ padding-top: 20px;
+ overflow-y: auto;
}
/* Styling for Autocomplete search bar */
@@ -50,20 +61,6 @@
.hidden {
display: none;
}
-
- /* Styling for an info pane that slides out from the left.
- * Hidden by default. */
- #panel {
- display: none;
- position: absolute;
- left: 0;
- top: 0;
- z-index: 1000;
- background-color: #fff;
- width: 300px;
- height: 100%;
- overflow-y: auto;
- }
.open {
width: 250px;
diff --git a/dist/js/map.js b/dist/js/map.js
index 5a13423..906c8a7 100644
--- a/dist/js/map.js
+++ b/dist/js/map.js
@@ -1,181 +1,81 @@
-/*
- * 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.
- */
+// 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, ...values) {
+ const entities = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
+ return strings.reduce((result, string, i) => {
+ const value = values[i - 1];
+ const escapedValue = String(value).replace(/[&<>'"]/g, (char) => entities[char]);
+ return result + escapedValue + string;
+ });
+}
+
+function initMap() {
+ const mapDivs = document.querySelectorAll('div[id^="map-"]');
+
+ mapDivs.forEach((mapDiv) => {
+ const mapId = mapDiv.id;
+ const json_link = mapDiv.getAttribute('data-link');
+ const panelId = mapId.replace('map-', 'panel-'); // Assuming panel ID is related to map ID
+ const key = mapDiv.getAttribute('data-key');
+ const unit = mapDiv.getAttribute('data-format');
-// 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;
- }
-
- /**
- * Initialize the Google Map.
- */
- function initMap() {
// Create the map.
- const map = new google.maps.Map(document.getElementById('map'), {
+ const map = new google.maps.Map(document.getElementById(mapId), {
zoom: 7, // Initial zoom, this will change based on locations
- center: {lat: 52.632469, lng: -1.689423}, // Initial center, this will also change
- styles: mapStyle,
+ center: { lat: 43.7376857, lng: -87.7226079 }, // Initial center, this will also change
+ //styles: mapStyle,
});
-
+
// Create a LatLngBounds object to calculate the map's bounds
const bounds = new google.maps.LatLngBounds();
-
+
// Load the stores GeoJSON onto the map.
- map.data.loadGeoJson('$link', {idPropertyName: 'storeid'}, function(features) {
- // Once the GeoJSON is loaded, iterate over each feature
- features.forEach(function(feature) {
- // Get the feature's geometry (its location)
+ map.data.loadGeoJson(json_link, { idPropertyName: 'storeid' }, function (features) {
+ features.forEach(function (feature) {
const geometry = feature.getGeometry();
-
- // If the geometry is a point, extend the bounds to include this point
if (geometry.getType() === 'Point') {
const coordinates = geometry.get();
bounds.extend(coordinates); // Extend the bounds to include this location
}
});
-
- // Fit the map's viewport to the bounds of the locations
map.fitBounds(bounds);
});
-
+
// Define the custom marker icons, using the store's "category".
const apiKey = '$key';
const infoWindow = new google.maps.InfoWindow();
-
- // 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 description = event.feature.getProperty('description') || ' ';
const hours = event.feature.getProperty('hours') || 'Hours not available';
const phone = event.feature.getProperty('phone') || 'Phone not available';
const position = event.feature.getGeometry().get();
-
+
const content = sanitizeHTML`
-
-
+
${name}
${description}
+
Category: ${category}
Open: ${hours} Phone: ${phone}
-
+
- `;
-
+ `;
+
infoWindow.setContent(content);
infoWindow.setPosition(position);
- infoWindow.setOptions({pixelOffset: new google.maps.Size(0, -30)});
+ infoWindow.setOptions({ pixelOffset: new google.maps.Size(0, -30) });
infoWindow.open(map);
});
-
+
// Build and add the search bar
const card = document.createElement('div');
const titleBar = document.createElement('div');
const title = document.createElement('div');
const container = document.createElement('div');
const input = document.createElement('input');
- const options = {
- types: ['address'],
- // componentRestrictions: {country: 'gb'},
- };
-
+ const options = { types: ['address'] };
+
card.setAttribute('id', 'pac-card');
title.setAttribute('id', 'title');
title.textContent = 'Find the nearest store';
@@ -188,79 +88,90 @@ const mapStyle = [{
card.appendChild(titleBar);
card.appendChild(container);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(card);
-
- // Make the search bar into a Places Autocomplete search bar and select
- // which detail fields should be returned about the place that
- // the user selects from the suggestions.
+
const autocomplete = new google.maps.places.Autocomplete(input, options);
-
- autocomplete.setFields(
- ['address_components', 'geometry', 'name']);
-
- // Set the origin point when the user selects an address
- const originMarker = new google.maps.Marker({map: map});
+ autocomplete.setFields(['address_components', 'geometry', 'name']);
+
+ const originMarker = new google.maps.Marker({ map: map });
originMarker.setVisible(false);
let originLocation = map.getCenter();
-
+
autocomplete.addListener('place_changed', async () => {
- originMarker.setVisible(false);
+ if (originMarker) {
+ originMarker.setMap(null); // Remove the previous marker if relevant
+ }
+
originLocation = map.getCenter();
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 address available for input: \'' + place.name + '\'');
+ window.alert(`No address available for input: '${place.name}'`);
return;
}
-
+
// Recenter the map to the selected address
originLocation = place.geometry.location;
map.setCenter(originLocation);
- map.setZoom(9);
- console.log(place);
-
- originMarker.setPosition(originLocation);
- originMarker.setVisible(true);
-
+
// Use the selected address as the origin to calculate distances
- // to each of the store locations
const rankedStores = await calculateDistances(map.data, originLocation);
- showStoresList(map.data, rankedStores);
-
+
+ // Filter stores by max radius of 60 miles (96,560 meters)
+ const maxRadiusMeters = 96560;
+ const filteredStores = rankedStores.filter(store => store.distanceVal <= maxRadiusMeters);
+
+ // Show the filtered stores in the relevant panel
+ showStoresList(map.data, filteredStores, panelId);
+
+ // Create a new LatLngBounds object to adjust the map bounds
+ const bounds = new google.maps.LatLngBounds();
+
+ // Extend the bounds to include the user's searched location
+ bounds.extend(originLocation);
+
+ // Extend the bounds to include each store location within the max radius
+ filteredStores.forEach((store) => {
+ const storeFeature = map.data.getFeatureById(store.storeid);
+ const storeLocation = storeFeature.getGeometry().get();
+ bounds.extend(storeLocation);
+ });
+
+ // Adjust the map to fit all markers within the bounds
+ map.fitBounds(bounds);
+
return;
});
- }
-
- /**
- * Use Distance Matrix API to calculate distance from origin to each store.
- * @param {google.maps.Data} data The geospatial data object layer for the map
- * @param {google.maps.LatLng} origin Geographical coordinates in latitude
- * and longitude
- * @return {Promise