src/map.js
import CleanupLocation from './cleanup_location';
import MarkerClusterer from 'marker-clusterer-plus';
import createBrowserHistory from 'history/createBrowserHistory';
import axios from 'axios';
import { mapOptions, MARKER_ICON_PATH, CLUSTER_MARKER_ICON_PATH, WORDPRESS_URL } from 'map_config';
let headerTemplate = require('./header.handlebars');
let searchTemplate = require('./search.handlebars');
let popupTemplate = require('./popup.handlebars');
let loadingTemplate = require('./loadingscreen.handlebars');
let InfoBox;
/**
* Free Seas Map
* @example
* let config = {
* element: document.getElementById('map'),
* searchBoxElement: document.getElementById('mapAutocomplete'),
* google: window.google,
* wordpressUrl: 'https://example-site/wp-json/wp/v2/tfs_map/'
* }
*
* window.freeSeasMap = new freeSeasMap(config);
*/
export default class Map {
/**
* Creates an instance of Map.
*
* @emits {map-loaded} emit event when google map has loaded
* @emits {map-loading-locations} emit event when loading locations
* @emits {map-cluster-rendered} emit event when cluster items have rendered [good for showing a loading window]
* @emits {map-idled} emit event when map has idled [good for grabbing updates on the locations on screen]
* @emits {map-place-changed} emit event when place has changed via auto complete search
* @emits {marker-clicked} emit event when map marker is clicked and data with that marker
* @emits {map-url-location} emit event when location_id is present and found as a valid location in the url
* @emits {map-info-window-closed} emit event when an info window is closed
*
* @param {Object} config - configuration
* @param {Object} config.element - document.getElementById("elementId") where to render the map
* @param {string} config.mapHeight - set the css height of the map default is 100vh
* @param {string} config.mapWidth - set the css width of the map default is 100vw
* @param {Object} config.google - google maps instance usually "window.google"
* @param {Object} config.searchBoxElement - document.getElementById("elementId") where to render the search bar
* @param {Object} config.searchBoxClickElement - document.getElementById("elementId") for element when clicked to trigger a search
* @param {string} config.infoWindowClickHandler - function to be called with the id of the cleanup location when its' info window is clicked
* @param {string} config.plusZoomControlImage - url for plus zoom control
* @param {string} config.minusZoomControlImage - url for plus zoom control
* @param {string} config.wordpressUrl - wordpress url to fetch data from
* @param {string} config.ocoMapUrl - original ocean conservancy map location
* @param {string} config.markerPlaceholderImagesUrl - If a location doesn't have an image, use a place holder image, if provided with photo01.png as the filename provide 30 to randomly cycle through in the directory, photo01.jpg~photo30.jpg
* @param {string} config.markerIconPath - path to individual image (32 x 32px default) for each marker on map
* @param {string} config.onClickMarkerIconPath - path to individual image (32 x 32px default) for each marker on map when clicked
* @param {string} config.clusterMarkerIconPath - path to individual image for cluster icons (64px x 64px default) on map
* @param {Object} config.customMapOptions - standard google maps options object, center, zoom, styles, etc @see https://developers.google.com/maps/documentation/javascript/reference/3.exp/map#Map
* @param {string} config.gestureHandling - Define the gesture behavior, default is "greedy" to allow scrolling without CTRL + Wheel or CMD + Wheel
* @param {Object} config.customClusterStyles - standard google cluster icon styles, use size, shape for images etc @see http://htmlpreview.github.io/?https://github.com/googlemaps/v3-utility-library/blob/master/markerclusterer/docs/reference.html
* @return {class} instance of Map
*
* @memberof Map
*/
constructor(config) {
/**
* DOM node to render map to
* @type {HTMLElement}
*/
this._element = config.element;
/**
* Map height, default is 100vh
* @type {string}
*/
this._mapHeight = config.mapHeight;
/**
* Map width, default is 100vw
* @type {string}
*/
this._mapWidth = config.mapWidth;
/**
* Map width, default is 100vw
* @type {string}
*/
/**
* Google Maps SDK Instance
* @type {<google>}
*/
this._google = config.google;
/**
* DOM node to place the places searchBox, should be of type input
* @type {HTMLElement}
*/
this._searchBoxElement = config.searchBoxElement;
/**
* DOM node to act as a click handler to fire search events on the searchBoxElement, button, div, span, etc
* @type {HTMLElement}
*/
this._searchBoxClickElement = config.searchBoxClickElement;
/**
* Function name to be called with the id of the cleanup location when its' info window is clicked, should be global
* @type {string}
*/
this._infoWindowClickHandler = config.infoWindowClickHandler;
/**
* URL path of image to use as the plus zoom control background image
* @type {string}
*/
this._plusZoomControlImage = config.plusZoomControlImage;
/**
* URL path of image to use as the minus zoom control background image
* @type {string}
*/
this._minusZoomControlImage = config.minusZoomControlImage;
/**
* URL path of the cleanup location data, @see https://oceanconservancy.org/
* wp-json/wp/v2/tfs_map/ is inferred as the wordpress api
* @type {string}
*/
this._wordpressUrl = config.wordpressUrl ? config.wordpressUrl : WORDPRESS_URL;
/**
* URL path of the Ocean Conservancy Map, defaults to hash links if none provided
*
* @type {string}
*/
this._ocoMapUrl = config.ocoMapUrl || '#';
/**
* URL path of the marker placeholder image, this are images to be displayed if a cleanup location doesn't have an image attached
* @type {string}
*/
this._markerPlaceholderImagesUrl = config.markerPlaceholderImagesUrl;
/**
* URL path of the marker icon that identify cleanup locations on the map, a 32px x 32px png or SVG is best
* @type {string}
*/
this._markerIconPath = config.markerIconPath ? config.markerIconPath : MARKER_ICON_PATH;
/**
* URL path of the cluster marker icon that identify multiple grouped cleanup locations on the map, a 64px x 64px png or SVG is best
* @type {string}
*/
this._clusterMarkerIconPath = config.clusterMarkerIconPath ? config.clusterMarkerIconPath : CLUSTER_MARKER_ICON_PATH;
/**
* URL path of the marker icon that changes when a marker is clicked, a 32px x 32px png or SVG is best
* @type {string}
*/
this._onClickMarkerIconPath = config.onClickMarkerIconPath;
/**
* A configuration object for defining google map related items
* @type {<google.maps.MapOptions>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3/map#MapOptions
*/
this._customMapOptions = config.customMapOptions ? Object.assign(mapOptions, config.customMapOptions) : mapOptions;
/**
* Embedded
* @type {boolean}
*/
this._embedded = config.embedded;
/**
* This setting controls how the API handles gestures on the map, default is greedy
* @type {string}
* @see https://developers.google.com/maps/documentation/javascript/reference/3/map#MapOptions.gestureHandling
*/
this._gestureHandling = config.gestureHandling ? config.gestureHandling : 'greedy';
/**
* Cluster styles default for cluster markers
* @type {Array}
* @see http://htmlpreview.github.io/?https://github.com/googlemaps/v3-utility-library/blob/master/markerclustererplus/docs/reference.html
*/
this._clusterStyles = [
{
textColor: 'white',
url: this._clusterMarkerIconPath,
height: 64,
width: 64
},
{
textColor: 'white',
url: this._clusterMarkerIconPath,
height: 64,
width: 64
},
{
textColor: 'white',
url: this._clusterMarkerIconPath,
height: 64,
width: 64
}
];
/**
* Merges the custom cluster styles with configured styles
* @type {Object}
* @see http://htmlpreview.github.io/?https://github.com/googlemaps/v3-utility-library/blob/master/markerclustererplus/docs/reference.html
*/
this._customClusterStyles = config.customClusterStyles ? config.customClusterStyles : this._clusterStyles;
/**
* Sets the MarkerClusterer to be used as a utility method to create marker clusters
* @type {<MarkerClusterer>}
* @see http://htmlpreview.github.io/?https://github.com/googlemaps/v3-utility-library/blob/master/markerclustererplus/docs/reference.html
*/
this._markerClusterer = MarkerClusterer;
/**
* Google Maps Utility
* @type {<google.maps.Map>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3/map
*/
this._map = null;
/**
* Google Maps Utility LatLngBounds, holds the last known valid center of the map
* @type {<google.maps.LatLngBounds>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3/coordinates#LatLngBounds
*/
this._lastValidCenter = null;
/**
* SearchBox element placeholder
* @type {HTMLElement}
*/
this._searchBox = null;
/**
* Active Marker that has been clicked on or hovered on the map
* @type {<google.maps.Marker>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3.exp/marker
*/
this._activeMarker = null;
/**
* Markers that have been fetched from the cleanup location database
* @type {<CleanupLocation[]>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3.exp/marker
*/
this._markers = [];
/**
* Visible markers that have been fetched from the cleanup location database in the current map view
* @type {<CleanupLocation[]>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3.exp/marker
*/
this._visibleMarkers = [];
/**
* Holds value if a search has been preformed
* @type {boolean}
*/
this._searchHistoryChanged = true;
/**
* Google Maps Info Window or InfoBox
* @type {<google.maps.InfoWindow>}
* @see https://developers.google.com/maps/documentation/javascript/reference/3.exp/marker
* @see https://github.com/googlemaps/v3-utility-library/tree/master/infobox
*/
this._infoWindow = null;
/**
* Max zoom level of the map
* @type {number}
*/
this._maxZoom = this._customMapOptions.maxZoom;
/**
* Min zoom level of the map
* @type {number}
*/
this._minZoom = this._customMapOptions.minZoom;
this._widgetPopupHandler = this._widgetPopupHandler.bind(this);
this._existingEmbedLocationHandler = this._existingEmbedLocationHandler.bind(this);
}
/**
* Fetches all locations from the wordpress url and places them in clusters on the map.
* Only viable markers are placed for performance reasons.
*
* @public
*
* @returns {undefined}
* @memberof Map
*/
fetchAllLocations() {
return new Promise((resolve, reject) => {
let locationRequests = [];
let options = {
page: 1,
per_page: 99, // eslint-disable-line camelcase
totalPages: 1
};
// get initial page number count
let requestOptions = {
method: 'GET',
baseURL: `${this._wordpressUrl}/wp-json/wp/v2/tfs_map/`,
responseType: 'json',
params: {
per_page: options.per_page, // eslint-disable-line camelcase
page: options.page
}
};
let pageCountRequest = axios.request(requestOptions);
pageCountRequest
.then(response => {
options.totalPages = response.headers['x-wp-totalpages'];
for (let i = 1; i <= options.totalPages; i++) {
options.page = i;
locationRequests.push(this._fetchLocations(options));
}
return axios.all(locationRequests);
})
.then(responses => {
return responses.concat.apply([], responses);
})
.then(data => {
resolve(data);
})
.catch(error => {
console.error(error);
});
});
}
/**
* Fetches batch of locations from wordpress url
* @private
*
* @param {Object} options
* @param {number} options.per_page - number of items to fetch
* @param {number} options.page - page number
* @returns {Promise<AxiosResponse[], AxiosError>}
* @memberof Map
*/
_fetchLocations(options) {
return new Promise((resolve, reject) => {
let requestOptions = {
method: 'GET',
baseURL: `${this._wordpressUrl}/wp-json/wp/v2/tfs_map/`,
responseType: 'json',
params: {
per_page: options.per_page, // eslint-disable-line camelcase
page: options.page
}
};
axios
.request(requestOptions)
.then(response => {
resolve(response.data);
})
.catch(error => {
console.error(error);
});
});
}
/**
* Method to fetch and set markers, util helper method
* @private
*
* @memberof Map
*/
_setMarkers() {
this._emitEvent('map-loading-locations');
this.fetchAllLocations()
.then(cleanupLocations => {
this._markers = this._setAllMarkers(cleanupLocations);
this._visibleMarkers = this._setAllVisibleMarkers(this._markers);
this._renderCluster(() => {
this._emitExistingLocation();
this._emitExistingSearch();
});
})
.catch(error => {
console.error(error);
});
}
/**
* Sets all markers on the map
* @private
*
* @param {any} locations - Wordpress Locations
* @param {<Map>} map - Active Google Map instance
* @returns {<Marker[]>} - Array of Google Map Marker
* @memberof Map
*/
_setAllMarkers(locations, map) {
this._markers = [];
for (let i = 0; i < locations.length; i++) {
var location = locations[i];
if (location.latitude && location.longitude) {
var marker = this._markerFactory(location, this._map);
this._markers.push(marker);
}
}
return this._markers;
}
/**
* Generate a random string integer between two integers
* @public
*
* @param {integer} start
* @param {integer} end
* @returns {string} padded number 01, 09, 10, 11 etc..
* @memberof Map
*/
getPaddedRandomNumberBetween(start, end) {
let randomNumber = Math.floor(Math.random() * end) + start;
if (randomNumber < 10) {
randomNumber = `0${randomNumber.toString()}`;
} else {
randomNumber.toString();
}
return randomNumber;
}
/**
* Renders custom zoom-in-btn as a google map control
* @private
*
* @param {Object} controlDiv - DOM element to attach the zoom control to
* @param {<Map>} map - Google Map instance
* @memberof Map
*/
_zoomInButton(controlDiv, map) {
let controlWrapper = document.createElement('div');
controlDiv.appendChild(controlWrapper);
let zoomInButton = document.createElement('div');
// Set CSS for the zoomIn
zoomInButton.className += 'zoom-in-button';
zoomInButton.style.backgroundImage = `url(${this._plusZoomControlImage})`;
controlDiv.appendChild(zoomInButton);
// Setup the click event listener - zoomIn
this._google.maps.event.addDomListener(zoomInButton, 'click', () => {
this.increaseZoom();
});
}
/**
* Renders custom zoom-out-btn as a google map control
* @private
*
* @param {Object} controlDiv - DOM element to attach the zoom control to
* @param {<Map>} map - Google Map instance
* @memberof Map
*/
_zoomOutButton(controlDiv, map) {
let zoomOutButton = document.createElement('div');
// Set CSS for the zoomOut
zoomOutButton.className += 'zoom-out-button';
zoomOutButton.style.backgroundImage = `url(${this._minusZoomControlImage})`;
controlDiv.appendChild(zoomOutButton);
// Setup the click event listener - zoomOut
this._google.maps.event.addDomListener(zoomOutButton, 'click', () => {
this.decreaseZoom();
});
}
/**
* Renders custom zoom-slider as a range input on the map
* @private
*
* @param {<Map>} map - Google Map instance
* @param {number} minZoom - set the min zoom level for the range slider, default is that setup with mapOptions
* @param {number} maxZoom - set the mx zoom level for the range slider, default is that setup with mapOptions
* @param {Object} controlDiv - DOM element to attach the zoom control to
* @memberof Map
*/
_zoomSlider(map, minZoom = 0, maxZoom = 21, controlDiv) {
maxZoom = this._maxZoom ? this._maxZoom : maxZoom;
minZoom = this._minZoom ? this._minZoom : minZoom;
let slider = document.createElement('input'),
zoomSlider = controlDiv;
slider.value = this._map.getZoom();
slider.setAttribute('min', minZoom);
slider.setAttribute('max', maxZoom);
slider.setAttribute('class', 'slider');
slider.setAttribute('step', '1');
slider.setAttribute('type', 'range');
slider.setAttribute('id', 'slide');
slider.onchange = event => {
this.updateSlider(event.target.value);
};
zoomSlider.appendChild(slider);
// Updates the slider when the zoom level of the map changes.
this._map.addListener('zoom_changed', () => {
let sl = document.getElementById('slide');
if (sl) {
if (sl.value !== this._map.getZoom()) {
sl.value = this._map.getZoom();
}
}
});
}
/**
* Increases map zoom by one level from the current zoom level
* @public
* @memberof Map
*/
increaseZoom() {
let currentZoom = this._map.getZoom();
this._map.setZoom(++currentZoom);
}
/**
* Decreases map zoom by one level from the current zoom level
* @public
* @memberof Map
*/
decreaseZoom() {
let currentZoom = this._map.getZoom();
this._map.setZoom(--currentZoom);
}
/**
* Update the zoom slider level with the current zoom amount, requires a zoom slider to be rendered on the map
* @public
* @memberof Map
*/
updateSlider(slideAmount) {
this._map.setZoom(parseInt(slideAmount, 10));
}
/**
* Checks to see if the map is embedded and returns a configuration
* @private
*
* @memberof Map
* @returns {<google.maps.MapOptions>}
*/
_shouldCenterOverNorthAmerica() {
let newCustomMapOptions = {};
// TODO: Cleanup this logic a bit
if (this._customMapOptions.center !== undefined) {
newCustomMapOptions = this._customMapOptions;
// if we are on a small screen we center over north america
// even if it was configured over another area unless the zoom is greater than 2
if (window.matchMedia('screen and (max-width: 768px)').matches) {
newCustomMapOptions = this._setDefaultMapLocation(39.809734, -98.555620, 3);
} else {
newCustomMapOptions = this._customMapOptions;
}
} else {
if (window.matchMedia('screen and (max-width: 768px)').matches) {
newCustomMapOptions = this._setDefaultMapLocation(39.809734, -98.555620, 3);
} else {
// set our default options otherwise
newCustomMapOptions = this._setDefaultMapLocation(35.623883, -39.459826, 3);
}
}
return newCustomMapOptions;
}
/**
* Sets the default map location based on the, lat, lng, and zoom.
* Uses the configured map location unless otherwise provided
* @private
*
* @param {number} lat
* @param {number} lng
* @param {number} zoom
*
* @memberof Map
*
* @returns {<google.maps.MapOptions>}
*/
_setDefaultMapLocation(lat, lng, zoom) {
let newCustomMapOptions = {};
newCustomMapOptions.center = this._customMapOptions.center || { lat: lat, lng: lng };
newCustomMapOptions.zoom = zoom;
newCustomMapOptions = Object.assign(this._customMapOptions, newCustomMapOptions);
return newCustomMapOptions;
}
/**
* Renders the google.maps.places.SearchBox with search and places geocoding
* Migrates the map to the location upon successful geocode
* @private
*
* @param {any} inputElement
* @memberof Map
*/
_setSearchBox(inputElement) {
if (this._infoWindow) {
this._infoWindow.close();
this._infoWindow = null;
}
this._searchBox = new this._google.maps.places.SearchBox(inputElement);
this._searchBox.setBounds(this._map.getBounds());
this._searchBox.addListener('places_changed', () => {
let places = this._searchBox.getPlaces();
if (places.length === 0) {
return;
}
let bounds = new this._google.maps.LatLngBounds();
places.forEach(place => {
if (!place || !place.geometry) return;
if (place.geometry.viewport) {
// Only geocoded places have viewport.
bounds.union(place.geometry.viewport);
} else {
bounds.extend(place.geometry.location);
}
});
let place = places[0];
let params = new URLSearchParams(location.search.slice(1));
this._map.fitBounds(bounds);
let placeId = params.get('place_id');
if (placeId) {
if (placeId !== place.id) {
window.mapHistory.push({
pathname: '/',
search: `?search=${this._searchBoxElement.value}&place_id=${place.id}`,
state: { type: 'search', value: this._searchBoxElement.value, place: place.id }
});
}
} else {
window.mapHistory.push({
pathname: '/',
search: `?search=${this._searchBoxElement.value}&place_id=${place.id}`,
state: { type: 'search', value: this._searchBoxElement.value, place: place.id }
});
}
var markerSearchResult = this._getVisibleMarkersInBounds(bounds, this._markers);
this._emitEvent('map-place-changed', markerSearchResult);
});
}
/**
* Sets the google.maps.event.trigger for focus and keydown on click of an element
* @private
*
* @param {Object} clickElement - DOM Element to be clicked on to trigger focus and keydown event for search
* @memberof Map
*/
_setSearchBoxClickElement(clickElement) {
clickElement.onclick = event => {
event.preventDefault();
this._google.maps.event.trigger(this._searchBoxElement, 'focus', {});
this._google.maps.event.trigger(this._searchBoxElement, 'keydown', { keyCode: 13 });
};
}
/**
* Event Emiter Helper
* Helps emit custom events, like loading, markers set, map idling etc
* @private
*
* @param {string} eventName - event name to be used
* @param {any} data - custom object data for the event
* @memberof Map
*/
_emitEvent(eventName, data) {
let event = null;
if (window.CustomEvent) {
event = new CustomEvent(eventName, { detail: data });
} else {
event = document.createEvent('CustomEvent');
event.initCustomEvent(eventName, true, true, data);
}
this._element.dispatchEvent(event);
}
/**
* Sets visible markers within map view on the map as a cluster
* @private
*
* @param {array} locations
* @returns {<Marker[]>} - google.maps.Marker
* @memberof Map
*/
_setAllVisibleMarkers(locations) {
this._visibleMarkers = [];
let bounds = this._map.getBounds();
let NE = bounds.getNorthEast();
let SW = bounds.getSouthWest();
for (var i = 0; i < locations.length; i++) {
var location = locations[i];
if (location.cleanupLocation.latitude() && location.cleanupLocation.longitude()) {
// Check if a place is within bounds - not best way, explained later
if (NE.lat() > location.cleanupLocation.latitude() && location.cleanupLocation.latitude() > SW.lat() && (NE.lng() > location.cleanupLocation.longitude() && location.cleanupLocation.longitude() > SW.lng())) {
this._visibleMarkers.push(location);
}
}
}
return this._visibleMarkers;
}
/**
* Sets visible markers within map view on the map as a cluster
* @private
*
* @param {<LatLngBounds>} bounds - Google Maps google.maps.LatLngBounds
* @param {<CleanupLocation[]>} locations - Array of Cleanup locations to filter
* @returns {<Marker[]>} - google.maps.Marker
* @memberof Map
*/
_getVisibleMarkersInBounds(bounds, locations) {
this._visibleMarkers = [];
// Magic Numbers
var expandedBounds = this._expandedBounds(bounds, -500, -500, -500, -500);
let NE = expandedBounds.getNorthEast();
let SW = expandedBounds.getSouthWest();
for (var i = 0; i < locations.length; i++) {
var location = locations[i];
if (location.cleanupLocation.latitude() && location.cleanupLocation.longitude()) {
// Check if a place is within bounds - not best way, explained later
if (NE.lat() > location.cleanupLocation.latitude() && location.cleanupLocation.latitude() > SW.lat() && (NE.lng() > location.cleanupLocation.longitude() && location.cleanupLocation.longitude() > SW.lng())) {
this._visibleMarkers.push(location);
}
}
}
return this._visibleMarkers;
}
/**
* Expands LatLngBounds based on a buffer param, padding is inverse, so positive numbers add inwards vs negative ones add
* @private
*
* @param {<LatLngBounds>} bounds - Google Maps google.maps.LatLngBounds
* @param {number} nPad - north padding buffer amount to be scaled 2^10
* @param {number} sPad - south padding buffer amount to be scaled 2^10
* @param {number} ePad - east padding buffer amount to be scaled 2^10
* @param {number} wPad - west padding buffer amount to be scaled 2^10
* @returns {<LatLngBounds>} - padded google.maps.LatLngBounds
* @memberof Map
*/
_expandedBounds(bounds, nPad, sPad, ePad, wPad) {
var SW = bounds.getSouthWest();
var NE = bounds.getNorthEast();
var topRight = this._map.getProjection().fromLatLngToPoint(NE);
var bottomLeft = this._map.getProjection().fromLatLngToPoint(SW);
var scale = Math.pow(2, 10);
var SWToPoint = this._map.getProjection().fromLatLngToPoint(SW);
var SWPoint = new this._google.maps.Point((SWToPoint.x - bottomLeft.x) * scale + wPad, (SWToPoint.y - topRight.y) * scale - sPad);
var SWWorld = new this._google.maps.Point(SWPoint.x / scale + bottomLeft.x, SWPoint.y / scale + topRight.y);
var pt1 = this._map.getProjection().fromPointToLatLng(SWWorld);
var NEToPoint = this._map.getProjection().fromLatLngToPoint(NE);
var NEPoint = new this._google.maps.Point((NEToPoint.x - bottomLeft.x) * scale - ePad, (NEToPoint.y - topRight.y) * scale + nPad);
var NEWorld = new this._google.maps.Point(NEPoint.x / scale + bottomLeft.x, NEPoint.y / scale + topRight.y);
var pt2 = this._map.getProjection().fromPointToLatLng(NEWorld);
return new this._google.maps.LatLngBounds(pt1, pt2);
}
/**
* Returns visible Google Map Markers with instances of CleanupLocation attached
* @public
*
* @returns {<Marker[]>} - google.maps.Marker
* @memberof Map
*/
visibleMarkers() {
return this._visibleMarkers;
}
/**
* Returns a cleanup location by its id
* @public
*
* @param {integer} id - cleanup location id
* @returns {<Marker>}
* @memberof Map
*/
cleanupLocationById(id) {
return this._markers.find(marker => {
return marker.cleanupLocation.id() === id;
});
}
/**
* Returns all markers in memory on the page with instances of CleanupLocation attached
* @public
*
* @returns {<Marker[]>} - google.maps.Marker
* @memberof Map
*/
allMarkers() {
return this._markers;
}
/**
* Open an default google maps info window by the id of a location
* @public
*
* @param {integer} markerId - Wordpress ID of the location from info.id
* @returns {Marker} Google Maps Marker
* @memberof Map
*/
openInfoWindowById(markerId) {
let foundMarker = null;
foundMarker = this._markers.find(location => {
if ('info' in location) {
return location.info.id === markerId;
}
return undefined;
});
if (foundMarker) {
this._resetMarkerIcon(this._activeMarker);
this._activeMarker = foundMarker;
if (this._infoWindow) {
this._infoWindow.open(this._map, this._activeMarker);
} else {
this._infoWindowFactory(this._activeMarker);
this._infoWindow.open(this._map, this._activeMarker);
}
}
return foundMarker;
}
/**
* Open a custom google maps info window by the id of a location, this method takes a google maps marker instead of an id
* @public
*
* @param {<Marker>} marker - google.maps.Marker object
* @param {string} contentString - HTML String to be displayed as a InfoBox @see https://github.com/googlemaps/v3-utility-library/tree/master/infobox
*
* @memberof Map
*/
openCustomInfoWindow(marker, contentString) {
if (marker) {
// set the active marker back to the standard icon
this._resetMarkerIcon(this._activeMarker);
if (this._infoWindow) {
this._infoWindow.close();
this._infoWindow = null;
this._emitEvent('map-info-window-closed');
}
this._infoWindow = new InfoBox({
content: contentString,
disableAutoPan: false,
maxWidth: 0,
alignBottom: false,
pixelOffset: new this._google.maps.Size(35, -100),
zIndex: null,
boxStyle: {
'box-shadow': '0 0 3px 0 rgba(0,0,0,0.50)',
opacity: 1,
zIndex: 999,
background: '#fff',
width: 'auto'
},
closeBoxMargin: '0px 0px 0px 0px',
closeBoxURL: '',
pane: 'floatPane',
enableEventPropagation: false,
infoBoxClearance: new this._google.maps.Size(100, 100)
});
this._google.maps.event.addListener(this._map, 'click', event => {
this._resetMarkerIcon(this._activeMarker);
if (this._infoWindow) {
this._infoWindow.close();
this._infoWindow = null;
this._emitEvent('map-info-window-closed');
}
});
// set the clicked on marker to the clicked on icon
if (this._onClickMarkerIconPath) {
let clickedOnImage = {
url: this._onClickMarkerIconPath,
// This marker is 20 pixels wide by 32 pixels high.
size: new this._google.maps.Size(32, 32),
// The origin for this image is (0, 0).
origin: new this._google.maps.Point(0, 0),
// The anchor for this image is the base of the flagpole at (0, 32).
anchor: new this._google.maps.Point(0, 32),
// The scaling size
scaledSize: new this._google.maps.Size(32, 32)
};
marker.setIcon(clickedOnImage);
}
this._infoWindow.open(this._map, marker);
try {
let params = new URLSearchParams(location.search.slice(1));
let locationId = parseInt(params.get('location_id'), 10);
if (locationId) {
if (locationId !== marker.cleanupLocation.id()) {
window.mapHistory.push({
pathname: '/',
search: `?location_id=${marker.cleanupLocation.id()}`,
state: { type: 'location', locationId: marker.cleanupLocation.id() }
});
}
} else {
window.mapHistory.push({
pathname: '/',
search: `?location_id=${marker.cleanupLocation.id()}`,
state: { type: 'location', locationId: marker.cleanupLocation.id() }
});
}
} catch (error) {
console.error(error);
}
this._activeMarker = marker;
} else {
throw new Error('No google maps marker provided');
}
}
/**
* Open a custom google maps info window by the id of a location, this method takes a google maps marker instead of an id
* this method also requires a callback that fires when the google maps dom is ready
* @public
*
* @param {<Marker>} marker - google.maps.Marker object
* @param {string} contentString - HTML String to be displayed as a InfoBox @see https://github.com/googlemaps/v3-utility-library/tree/master/infobox
* @param {function} callback - callback to fire once the dom is ready and the info window is displayed
*
* @memberof Map
*/
openCustomInfoWindowAsync(marker, contentString, callback) {
if (marker) {
// this._activeMarker = marker;
this._resetMarkerIcon(this._activeMarker);
if (this._infoWindow) {
this._infoWindow.close();
this._infoWindow = null;
this._emitEvent('map-info-window-closed');
}
this._infoWindow = new InfoBox({
content: contentString,
disableAutoPan: false,
maxWidth: 0,
alignBottom: false,
pixelOffset: new this._google.maps.Size(35, -100),
zIndex: null,
boxStyle: {
'box-shadow': '0 0 3px 0 rgba(0,0,0,0.50)',
opacity: 1,
zIndex: 999,
background: '#fff',
width: 'auto'
},
closeBoxMargin: '0px 0px 0px 0px',
closeBoxURL: '',
pane: 'floatPane',
enableEventPropagation: false,
infoBoxClearance: new this._google.maps.Size(100, 100)
});
this._google.maps.event.addListener(this._map, 'click', event => {
this._resetMarkerIcon(this._activeMarker);
if (this._infoWindow) {
this._infoWindow.close();
this._infoWindow = null;
this._emitEvent('map-info-window-closed');
}
});
// set the clicked on marker to the clicked on icon
if (this._onClickMarkerIconPath) {
let clickedOnImage = {
url: this._onClickMarkerIconPath,
// This marker is 20 pixels wide by 32 pixels high.
size: new this._google.maps.Size(32, 32),
// The origin for this image is (0, 0).
origin: new this._google.maps.Point(0, 0),
// The anchor for this image is the base of the flagpole at (0, 32).
anchor: new this._google.maps.Point(0, 32),
// The scaling size
scaledSize: new this._google.maps.Size(32, 32)
};
marker.setIcon(clickedOnImage);
}
this._infoWindow.open(this._map, marker);
try {
let params = new URLSearchParams(location.search.slice(1));
let locationId = parseInt(params.get('location_id'), 10);
if (locationId) {
if (locationId !== marker.cleanupLocation.id()) {
window.mapHistory.push({
pathname: '/',
search: `?location_id=${marker.cleanupLocation.id()}`,
state: { type: 'location', locationId: marker.cleanupLocation.id() }
});
}
} else {
window.mapHistory.push({
pathname: '/',
search: `?location_id=${marker.cleanupLocation.id()}`,
state: { type: 'location', locationId: marker.cleanupLocation.id() }
});
}
} catch (error) {
console.error(error);
}
this._google.maps.event.addListener(this._infoWindow, 'domready', () => {
callback();
});
} else {
throw new Error('No google maps marker provided');
}
}
/**
* Resets the active marker onclick image set from the config.onClickMarkerIconPath
*
* @private
* @param {<Marker>} activeMarker - active marker instance or is found from the Map instances activeMarker
*
* @memberof Map
*/
_resetMarkerIcon(activeMarker) {
if (this._onClickMarkerIconPath && this._activeMarker) {
let image = {
url: this._markerIconPath,
// This marker is 20 pixels wide by 32 pixels high.
size: new this._google.maps.Size(32, 32),
// The origin for this image is (0, 0).
origin: new this._google.maps.Point(0, 0),
// The anchor for this image is the base of the flagpole at (0, 32).
anchor: new this._google.maps.Point(0, 32),
// The scaling size
scaledSize: new this._google.maps.Size(32, 32)
};
activeMarker.setIcon(image);
}
}
/**
* Closes the active info window if it is open and exists
*
* @public
*
* @memberof Map
*/
closeCustomInfoWindow() {
if (this._infoWindow) {
this._infoWindow.close();
}
}
/**
* Returns if an info window is open and exists
*
* @public
* @return {boolean} if window is open, true, else false
* @memberof Map
*/
isInfoWindowOpen() {
if (this._infoWindow) {
return true;
}
return false;
}
/**
* Centers Map to lat, lng points using google.map.panTo
* @public
*
* @param {string} lat
* @param {string} lng
* @memberof Map
*/
panToLatLng(lat, lng) {
let latLng = new this._google.maps.LatLng(lat, lng);
this._map.panTo(latLng);
this._map.setZoom(14);
}
/**
* Centers Map to lat, lng points using google.map.panTo, fires a callback once the map tiles have loaded
* @public
*
* @param {string} lat
* @param {string} lng
* @param {function} callback
* @memberof Map
*/
panToLatLngAsync(lat, lng, callback) {
let latLng = new this._google.maps.LatLng(lat, lng);
this._map.panTo(latLng);
this._map.setZoom(14);
this._google.maps.event.addListener(this._map, 'tilesloaded', () => {
callback();
});
}
/**
* Sets up listeners for out of bounds when map is panned
* @private
*
* @memberof Map
*/
_setOutOfBoundsListener() {
this._google.maps.event.addListener(this._map, 'dragend', () => {
this._checkLatitude();
});
this._google.maps.event.addListener(this._map, 'idle', () => {
this._checkLatitude();
});
this._google.maps.event.addListener(this._map, 'zoom_changed', () => {
this._checkLatitude();
});
}
/**
* Checks latitude from the valid center set between -85 and 85 degrees latitude
* @private
*
* @memberof Map
*/
_checkLatitude() {
if (this._minZoom) {
if (this._map.getZoom() < this._minZoom) {
this._map.setZoom(parseInt(this._minZoom, 10));
}
}
let SW = this._map
.getBounds()
.getSouthWest()
.lat();
let NE = this._map
.getBounds()
.getNorthEast()
.lat();
if (SW < -85 || NE > 85) {
if (this._lastValidCenter) {
this._map.setCenter(this._lastValidCenter);
}
} else {
this._lastValidCenter = this._map.getCenter();
}
}
/**
* Checks for location_id as a URL param and emits and event with the cleanup location marker
*
* @private
* @memberof Map
*/
_emitExistingLocation() {
try {
let params = new URLSearchParams(location.search.slice(1));
let locationId = params.get('location_id');
if (locationId) {
let marker = this.cleanupLocationById(parseInt(locationId, 10));
this._activeMarker = marker;
this._emitEvent('map-url-location', this._activeMarker);
}
} catch (error) {
console.error(error);
}
}
/**
* Checks for search as a query param in the URL and preforms a search if it exists
*
* @private
* @param searchValue - text content that should be sent to the google places api via config.searchBoxElement
* @return {string} search text from the URL or that provided to the method
* @memberof Map
*/
_emitExistingSearch(searchValue) {
let searchParams;
if (searchValue === undefined) {
let params = new URLSearchParams(location.search.slice(1));
searchParams = params.get('search');
}
if (searchParams || searchValue) {
this._searchBoxElement.value = '';
this._searchBoxElement.value = searchValue || searchParams;
this._google.maps.event.trigger(this._searchBoxElement, 'focus', {});
this._google.maps.event.trigger(this._searchBoxElement, 'keydown', { keyCode: 13 });
return searchParams;
}
return searchParams;
}
/**
* Returns a Google Maps Marker based on a wordpress data
* @private
*
* @param {any} data
* @returns {Marker} - Google Maps Marker
* @memberof Map
*/
_markerFactory(data) {
let image = {
url: this._markerIconPath,
// This marker is 20 pixels wide by 32 pixels high.
size: new this._google.maps.Size(32, 32),
// The origin for this image is (0, 0).
origin: new this._google.maps.Point(0, 0),
// The anchor for this image is the base of the flagpole at (0, 32).
anchor: new this._google.maps.Point(0, 32),
// The scaling size
scaledSize: new this._google.maps.Size(32, 32)
};
let baseMarkerOptions;
try {
let cleanupLocation = new CleanupLocation(data);
let latLng = new this._google.maps.LatLng(cleanupLocation.latitude(), cleanupLocation.longitude());
baseMarkerOptions = Object.assign(
{
position: latLng,
icon: image,
cleanupLocation
},
{ info: data }
);
let marker = new this._google.maps.Marker(baseMarkerOptions);
marker.addListener('mouseover', () => {
this._emitEvent('marker-hover', marker);
});
marker.addListener('click', () => {
// reset previous active marker if it exists
this._resetMarkerIcon(this._activeMarker);
// define new marker and set the clicked on path
this._activeMarker = marker;
if (this._onClickMarkerIconPath) {
let clickedOnImage = {
url: this._onClickMarkerIconPath,
// This marker is 20 pixels wide by 32 pixels high.
size: new this._google.maps.Size(32, 32),
// The origin for this image is (0, 0).
origin: new this._google.maps.Point(0, 0),
// The anchor for this image is the base of the flagpole at (0, 32).
anchor: new this._google.maps.Point(0, 32),
// The scaling size
scaledSize: new this._google.maps.Size(32, 32)
};
marker.setIcon(clickedOnImage);
}
this._emitEvent('marker-clicked', this._activeMarker);
});
return marker; // eslint-disable-line consistent-return
} catch (error) {
console.error(error);
return; // eslint-disable-line consistent-return
}
}
/**
* Creates google.maps.InfoWindow instance
* @private
*
* @param {Marker} marker - Google Maps Marker
* @returns {InfoWindow} - Google Maps Info Window
* @memberof Map
*/
_infoWindowFactory(marker) {
let contentString = '';
if (this._infoWindowClickHandler) {
contentString += `<div class="popover__container" onclick="${this._infoWindowClickHandler}(${marker.cleanupLocation.id()})">`;
} else {
contentString += `<div class="popover__container">`; // eslint-disable-line quotes
}
contentString += `
<div class="popup">
<div class="popup__image">
`;
if (marker.cleanupLocation.image() && marker.cleanupLocation.image() !== '') {
contentString += `
<img src=${marker.cleanupLocation.image()} alt="">
`;
} else {
var imageUrl = `${this._markerPlaceholderImagesUrl}/photo${this.getPaddedRandomNumberBetween(1, 30)}.jpg`;
contentString += `
<img src=${imageUrl} alt="">
`;
}
contentString += `
</div>
<div class="popup__content">
<h3 class="popup__title">${marker.cleanupLocation.title()}</h3>
<h4 class="popup__org">${marker.cleanupLocation._locationInformation.partnerName}</h4>
<address class="popup__location">${marker.cleanupLocation.city()}, ${marker.cleanupLocation.state()}
<img class="dot" src="/wp-content/themes/oco-wp/images/map/dot.png" width="5" height="5" alt="">${marker.cleanupLocation.country()}</address>
</div>
</div>
</div>
`;
this._infoWindow = new this._google.maps.InfoWindow({
content: contentString
});
return this._infoWindow;
}
/**
* Creates google.maps.InfoBox instance
* @private
* @deprecated - use openCustomInfoWindow or openCustomInfoWindowAsync instead
*
* @param {string} htmlContent - HTML string to set to the content of the InfoWindow
* @returns {<InfoWindow>} - Google Maps InfoWindow
* @memberof Map
*/
_customInfoWindowFactory(htmlContent) {
this._infoWindow = new this._google.maps.InfoWindow({
content: htmlContent
});
return this._infoWindow;
}
/**
* Renders the marker cluster on the map
* @private
*
* @returns {object} MarkerCluster instance
* @memberof Map
*/
_renderCluster(callback) {
let markerCluster = new this._markerClusterer(this._map, this._visibleMarkers, {
gridSize: 50,
maxZoom: 7,
imagePath: this._clusterMarkerIconPath,
styles: this._customClusterStyles
});
if (callback) {
this._emitEvent('map-cluster-rendered');
callback(markerCluster);
}
}
/**
* Checks to see if window.google is available and waits to render the map when it is
* @private
*
* @param {function} callback - callback to fire when window.google has loaded
* @memberof Map
*/
_isGoogleMapsLoaded(callback) {
if (typeof window.google === 'object' && typeof window.google.maps === 'object') {
callback();
} else {
setTimeout(function () {
this._isGoogleMapsLoaded(callback);
}, 250);
}
}
/**
* Render the map countdown header on the element given in the instance of the map, used for embedded freeSeas maps
* @private
*
* @return {Promise<undefined, AxiosError>}
* @memberof Map
*/
_renderHeader() {
return new Promise((resolve, reject) => {
let displayData = Object.assign({ wordpressUrl: this._wordpressUrl });
let div = headerTemplate(displayData);
document.body.insertAdjacentHTML('beforeend', div);
axios
.get(`${this._wordpressUrl}/wp-json/wp/v2/acf/options`)
.then(response => {
displayData = Object.assign({ wordpressUrl: this._wordpressUrl }, response.data);
let countDownDate = new Date(displayData.countdown_date).getTime();
div = headerTemplate(displayData);
document.getElementById('headerPanel').outerHTML = div;
setInterval(() => {
let now = new Date().getTime();
let distance = countDownDate - now;
let days = Math.floor(distance / (1000 * 60 * 60 * 24));
let hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
if (hours.toString().length < 2) {
hours = '0' + hours;
}
let minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
if (minutes.toString().length < 2) {
minutes = '0' + minutes;
}
document.getElementById('days').innerHTML = '<span>' + days + '</span>';
document.getElementById('hours').innerHTML = '<span>' + hours + '</span>';
document.getElementById('minutes').innerHTML = '<span>' + minutes + '</span>';
if (distance < 0) {
document.getElementById('days').innerHTML = '<span>00</span>';
document.getElementById('hours').innerHTML = '<span>00</span>';
document.getElementById('minutes').innerHTML = '<span>00</span>';
}
}, 1000);
resolve();
})
.catch(error => {
reject(error);
});
});
}
/**
* Render the map searchBar on the element given in the instance of the map, used for embedded freeSeas maps
* @private
*
* @memberof Map
*/
_renderSearch() {
let div = searchTemplate({
wordpressUrl: this._wordpressUrl
});
let body = document.getElementsByTagName('body')[0];
body.insertAdjacentHTML('beforeend', div);
this._searchBoxElement = document.getElementById('mapAutocomplete');
this._searchBoxClickElement = document.getElementById('mapSearchSubmit');
}
_renderLoading() {
let div = loadingTemplate();
let body = document.getElementsByTagName('body')[0];
body.insertAdjacentHTML('beforeend', div);
}
/**
* Handles infoWindow popup logic for embedded freeSeas maps, opens a custom handlebars InfoBox with a link to
* the ocean conservancy map
* @private
*
* @memberof Map
*/
_widgetPopupHandler(event) {
let marker = event.detail;
if (!marker.cleanupLocation.image() || marker.cleanupLocation.image() === '') {
let randomNumber = this.getPaddedRandomNumberBetween(1, 30);
let imageUrl = `${this._markerPlaceholderImagesUrl}/photo${randomNumber}.jpg`;
marker.cleanupLocation._locationInformation.image = imageUrl;
marker.cleanupLocation._locationInformation.isFillImage = true;
}
marker.cleanupLocation._locationInformation.dotImage = `${this._markerPlaceholderImagesUrl}/dot.png`;
marker.cleanupLocation._locationInformation.locationLink = `${this._ocoMapUrl}?location_id=${marker.cleanupLocation.id()}`;
let compiledPopupTemplate = popupTemplate(marker.cleanupLocation._locationInformation);
// TODO: Figure out this weird side effect, have to call the method twice to actually open the info window on first click
this.openCustomInfoWindow(marker, compiledPopupTemplate);
this.openCustomInfoWindow(marker, compiledPopupTemplate);
}
_existingEmbedLocationHandler(event) {
let marker = event.detail;
if (!marker.cleanupLocation.image() || marker.cleanupLocation.image() === '') {
let randomNumber = this.getPaddedRandomNumberBetween(1, 30);
let imageUrl = `${this._markerPlaceholderImagesUrl}/photo${randomNumber}.jpg`;
marker.cleanupLocation._locationInformation.image = imageUrl;
marker.cleanupLocation._locationInformation.isFillImage = true;
}
marker.cleanupLocation._locationInformation.dotImage = `${this._markerPlaceholderImagesUrl}/dot.png`;
marker.cleanupLocation._locationInformation.locationLink = `${this._ocoMapUrl}?location_id=${marker.cleanupLocation.id()}`;
let compiledPopupTemplate = popupTemplate(marker.cleanupLocation._locationInformation);
this.panToLatLng(marker.cleanupLocation.latitude(), marker.cleanupLocation.longitude());
// TODO: Figure out this weird side effect, have to call the method twice to actually open the info window on first click
this.openCustomInfoWindow(marker, compiledPopupTemplate);
this.openCustomInfoWindow(marker, compiledPopupTemplate);
}
/**
* Main rendering logic of the freeSeas map widget, here history is set for the map as well as various tasks to listen for google maps
* DOM events and merge configuration logic with the initial setup of a freeSeas maps
* @public
*
* @memberof Map
*/
render() {
InfoBox = require('google-maps-infobox');
this._isGoogleMapsLoaded(() => {
// Check if our options should change based on the map type and custom map options provided
let mapOptions = this._shouldCenterOverNorthAmerica();
this._customMapOptions = Object.assign(mapOptions, { gestureHandling: this._gestureHandling });
if (this._element) {
this._map = new this._google.maps.Map(this._element, this._customMapOptions);
} else {
this._renderLoading();
this._renderHeader();
this._renderSearch();
this._element = document.createElement('div');
this._element.setAttribute('id', 'map');
let body = document.getElementsByTagName('body')[0];
body.appendChild(this._element);
this._map = new this._google.maps.Map(this._element, this._customMapOptions);
// set events
this._element.addEventListener('marker-clicked', this._widgetPopupHandler);
this._element.addEventListener('map-cluster-rendered', () => {
let element = document.getElementById('preloader');
element.style.display = 'none';
});
this._element.addEventListener('map-location-history-pushed', this._widgetPopupHandler);
this._element.addEventListener('map-location-history-popped', this._widgetPopupHandler);
this._element.addEventListener('map-url-location', this._existingEmbedLocationHandler);
}
this._google.maps.event.addListener(this._map, 'click', event => {
this._resetMarkerIcon(this._activeMarker);
});
// set search input and on click handler
if (this._searchBoxElement) {
this._setSearchBox(this._searchBoxElement);
if (this._searchBoxClickElement) {
this._setSearchBoxClickElement(this._searchBoxClickElement);
}
}
// Set History
const history = createBrowserHistory({
basename: location.pathname, // The base URL of the app (see below)
keyLength: 6 // Set true to force full page refreshes
});
window.mapHistory = history;
window.mapHistory.listen((location, action) => {
this._resetMarkerIcon(this._activeMarker);
this.closeCustomInfoWindow();
if (location.state) {
if (location.state.type === 'location') {
let marker;
switch (action) {
case 'PUSH':
marker = this.cleanupLocationById(parseInt(location.state.locationId, 10));
this._emitEvent('map-location-history-pushed', marker);
break;
case 'POP':
marker = this.cleanupLocationById(parseInt(location.state.locationId, 10));
this._emitEvent('map-location-history-popped', marker);
break;
default:
this._resetMarkerIcon(this._activeMarker);
break;
}
} else if (location.state.type === 'search') {
this._emitExistingSearch(location.state.value);
this._emitEvent('map-search-history-changed', location.state.value);
}
} else {
try {
let params = new URLSearchParams(location.search.slice(1));
let locationId = parseInt(params.get('location_id'), 10);
if (locationId) {
let marker;
switch (action) {
case 'PUSH':
marker = this.cleanupLocationById(parseInt(locationId, 10));
this._emitEvent('map-location-history-pushed', marker);
break;
case 'POP':
marker = this.cleanupLocationById(parseInt(locationId, 10));
this._emitEvent('map-location-history-popped', marker);
break;
default:
this._resetMarkerIcon(this._activeMarker);
break;
}
} else {
if (location.state) {
this._emitEvent('map-search-history-changed', location.state.value);
}
}
} catch (error) {
console.error(error);
}
}
});
// Set zoom controls
let zoomControlDiv = document.createElement('div');
let zoomInButton = this._zoomInButton(zoomControlDiv, this._map); // eslint-disable-line no-unused-vars
let zoomSlider = this._zoomSlider(this._map, this._minZoom, this._maxZoom, zoomControlDiv); // eslint-disable-line no-unused-vars
let zoomOutButton = this._zoomOutButton(zoomControlDiv, this._map); // eslint-disable-line no-unused-vars
zoomControlDiv.index = 1;
this._map.controls[this._google.maps.ControlPosition.RIGHT_BOTTOM].push(zoomControlDiv);
// Set Out Of Bounds Handler
this._setOutOfBoundsListener();
setTimeout(() => {
this._setMarkers(this._map);
this._emitEvent('map-loaded');
}, 1000);
// Wait for the map to idle before loading more points
setTimeout(() => {
this._google.maps.event.addListener(this._map, 'tilesloaded', () => {
this._setAllVisibleMarkers(this._markers);
// Render All Points
this._renderCluster();
// Idle the Map
this._emitEvent('map-idled', this._visibleMarkers);
});
}, 2000);
});
}
}