app/javascript/app/util/map/google/map-renderer.js
// Manages search results view Google Map.
import markers from 'app/util/map/markers.js.erb';
import markerDataLoader from 'app/util/map/marker-data-loader';
import MapDOM from 'app/util/map/google/MapDOM';
import infoBoxManager from 'app/util/map/google/infobox-manager';
const { InfoBox } = require("google-maps-infobox");
const { OverlappingMarkerSpiderfier } = require('app/util/map/google/oms.min');
// Module instance with map DOM and google.maps.Map instance references.
var _mapDOM;
// Whether the map is at its max size or not.
var _atMaxSize = false;
// The collective map bounds of the markers.
var _markerBounds;
// The spiderfier layer for handling overlapping markers.
// See https://github.com/jawj/OverlappingMarkerSpiderfier
var _spiderfier;
// The marker the cursor is currently over.
var _overMarker;
// @param mapContainerSelector [String] DOM selector for the map container.
// @param mapCanvasSelector [String] DOM selector for the map canvas.
function init(mapContainerSelector, mapCanvasSelector) {
_mapDOM = MapDOM.create(mapContainerSelector, mapCanvasSelector);
}
// Run all rendering methods for the map, markers, and info box.
// Also register events for interactivity.
function renderMapStack() {
_mapDOM.render();
_renderMarkers();
infoBoxManager.render(_mapDOM, InfoBox);
_registerMapEvents();
infoBoxManager.registerInfoBoxEvents();
}
// Toggle size of map.
function toggleSize() {
_atMaxSize = !_atMaxSize;
if (_atMaxSize)
_mapDOM.canvas.classList.add('max');
else
_mapDOM.canvas.classList.remove('max');
_updateMarkerSizes();
_refresh();
}
// @param OverlappingMarkerSpiderfier [Object] Marker spiderfier plugin.
function _renderMarkers() {
var spiderfierOptions = {
legWeight: 2,
circleFootSeparation: 30,
keepSpiderfied: true,
nearbyDistance: 40
};
_spiderfier = new OverlappingMarkerSpiderfier(_mapDOM.map,
spiderfierOptions);
// Initialize marker bounds.
_markerBounds = new google.maps.LatLngBounds();
// Loop over marker data and initialize each marker.
var markerData = markerDataLoader.getData();
var index = markerData.length - 1;
var marker;
while (index >= 0) {
marker = _loadMarker(markerData[index--], _atMaxSize, _spiderfier);
}
_overMarker = marker;
_refresh();
}
// Load a single map marker.
// @returns [Object] A google.maps.Marker instance that was created,
// or null if there isn't a latitude and longitude in the data.
function _loadMarker(markerData, atMaxSize, spiderfier) {
if (!markerData.latitude && !markerData.longitude) return;
var myLatLng = new google.maps.LatLng(markerData.latitude,
markerData.longitude);
var markerProxy = markers.create(markers.GENERIC);
if (atMaxSize)
markerProxy.turnOn(markerProxy.LARGE_ICON);
else
markerProxy.turnOn(markerProxy.SMALL_ICON);
var markerOptions = {
map: _mapDOM.map,
position: myLatLng,
icon: markerProxy.getIcon(),
optimized: false,
manager: markerProxy
};
var marker = new google.maps.Marker(markerOptions);
spiderfier.addMarker(marker);
var mainName = markerData.name;
var orgName = markerData.org_name;
var agency = '';
if (orgName !== markerData.name)
agency = '<h2>' + orgName + '</h2>';
var content = "<div><div class='button-close'></div>" +
'<h1>' + mainName + '</h1>' + agency +
'<p>' + markerData.street_address + ', ' +
markerData.city + '</p>' + "<p><a href='/locations/" +
markerData.slug+(window.location.search) +
"'>View more details…</a></p></div>";
_registerMarkerEvents(marker, content, spiderfier);
_markerBounds.extend(myLatLng);
return marker;
}
// Register events for map interactivity.
function _registerMapEvents() {
google.maps.event.addListener(_mapDOM.map, 'idle', _mapIdle);
google.maps.event.addListener(_mapDOM.map, 'click', _mapClick);
_mapDOM.canvas.addEventListener('touchstart', _mapTouch, false);
}
// Event handler for when the map is idle. This is used by the spiderfier
// to style the map markers that will spiderfy when clicked.
function _mapIdle() {
_setAllIcons();
google.maps.event.addListener(_mapDOM.map, 'zoom_changed', _mapZoomed);
// Remove idle listeners as they aren't needed after the spiderfied markers
// are styled for the first time.
google.maps.event.clearListeners(_mapDOM.map, 'idle');
}
// Event handler for when the map is zoomed. This is used by the spiderfier
// to handle style changes to the map markers that will spiderfy when clicked.
function _mapZoomed() {
_setAllIcons();
}
// Event handler for when the map is clicked. This is used to close
// any currently open info box.
function _mapClick() {
infoBoxManager.turnOffInfoBoxStates();
infoBoxManager.updateInfoBoxState(_overMarker, 0);
}
// Event handler for when a touch event occurs on the map, for
// changing the interactivity to accommodate lack of mouseover/out events.
function _mapTouch() {
infoBoxManager.turnOn(infoBoxManager.STATE.IS_TOUCH);
_mapDOM.canvas.removeEventListener('touchstart', _mapTouch, false);
}
// Run through the markers and set them to a spiderfied large or small
// appearance based on the size of the map.
function _setAllIcons() {
// Style all markers.
var markers = _spiderfier.getMarkers();
var index = markers.length - 1;
while(index >= 0) {
_setIcon(markers[index--], false);
}
// Style spiderfier markers.
markers = _spiderfier.markersNearAnyOtherMarker();
index = markers.length - 1;
while(index >= 0) {
_setIcon(markers[index--], true);
}
}
// Set the icon for a marker to the large or small regular
// or spiderfied marker.
// @param marker [Object] a map marker.
// @param useSpiderfied [Boolean] true if the spiderfied marker should
// be used, false otherwise.
function _setIcon(marker, useSpiderfied) {
var manager = marker.manager;
if (useSpiderfied) {
if (_atMaxSize)
manager.turnOn(manager.LARGE_ICON | manager.SPIDERFIED_ICON);
else
manager.turnOn(manager.SMALL_ICON | manager.SPIDERFIED_ICON);
}
else {
if (_atMaxSize)
manager.turnOn(manager.LARGE_ICON | manager.UNSPIDERFIED_ICON);
else
manager.turnOn(manager.SMALL_ICON | manager.UNSPIDERFIED_ICON);
}
marker.setIcon(manager.getIcon());
}
// Updates the marker icons to the size set for the map.
function _updateMarkerSizes() {
var markers = _spiderfier.getMarkers();
var index = markers.length - 1;
var marker;
while (index >= 0) {
marker = markers[index--];
marker.setIcon(marker.manager.getIcon());
}
}
// Make info box events associated with a map marker.
// @param marker [Object] The marker that triggered the opening of the
// info box.
// @param content [String] The text content of the info box.
// @param spiderfier [Object] The spiderfier plugin instance.
function _registerMarkerEvents(marker, content, spiderfier) {
// Change marker icon appearances when the markers spiderfy.
spiderfier.addListener('spiderfy',
function(spiderfied,
unspiderfied) {
infoBoxManager.turnOn(infoBoxManager.STATE.HAS_SPIDERFIED);
var index = spiderfied.length - 1;
while (index >= 0) {
_setIcon(spiderfied[index--], false);
}
});
// Change marker icon appearances when the markers unspiderfy.
spiderfier.addListener('unspiderfy',
function(spiderfied,
unspiderfied) {
var index = spiderfied.length - 1;
while (index >= 0) {
_setIcon(spiderfied[index--], true);
}
});
// Register the marker the cursor has rolled over.
google.maps.event.addListener(marker, 'mouseover', function() {
if (_overMarker !== marker)
infoBoxManager.turnOff(infoBoxManager.STATE.PIN_INFOBOX);
if (infoBoxManager.isOff(infoBoxManager.STATE.PIN_INFOBOX)) {
_registerMarker(marker, content);
infoBoxManager.updateInfoBoxState(_overMarker);
}
});
// Unregister the marker the cursor rolled out of.
google.maps.event.addListener(marker, 'mouseout', function() {
infoBoxManager.turnOff(infoBoxManager.STATE.OVER_MARKER);
infoBoxManager.turnOff(infoBoxManager.STATE.OVER_SPIDERFY_MARKER);
infoBoxManager.updateInfoBoxState(_overMarker);
});
// When user clicks the marker, open the infoBox
// and center the map on the marker,
// unless the user clicked a marker that just spiderfied.
google.maps.event.addListener(marker, 'click', function() {
// Touch displays don't know they're over a marker till it's tapped,
// so manually register the state as being over the marker in this case.
if (infoBoxManager.isOn(infoBoxManager.STATE.IS_TOUCH))
_registerMarker(marker, content);
if (infoBoxManager.isOn(infoBoxManager.STATE.HAS_SPIDERFIED)) {
infoBoxManager.turnOff(infoBoxManager.STATE.HAS_SPIDERFIED);
}
else if (infoBoxManager.isOn(infoBoxManager.STATE.PIN_INFOBOX)){
infoBoxManager.turnOffInfoBoxStates();
infoBoxManager.updateInfoBoxState(_overMarker, 0);
}
else {
_mapDOM.map.panTo(marker.position);
infoBoxManager.turnOn(infoBoxManager.STATE.PIN_INFOBOX);
infoBoxManager.turnOn(infoBoxManager.STATE.OVER_MARKER);
infoBoxManager.turnOff(infoBoxManager.STATE.OVER_SPIDERFY_MARKER);
infoBoxManager.updateInfoBoxState(_overMarker, 0);
}
});
}
// Whether a map marker is a spiderfied marker.
// @param marker [Object] a map marker.
// @return [Boolean] true if the marker is spiderfied, false otherwise.
function _isSpiderfyMarker(marker) {
var manager = marker.manager;
return manager.isOn(manager.SPIDERFIED_ICON);
}
// Register a marker as having been clicked.
// @param marker [Object] The marker that was clicked.
// @param content [String] The text content of the infobox for this marker.
function _registerMarker(marker, content) {
_overMarker = marker;
if (_isSpiderfyMarker(marker))
infoBoxManager.turnOn(infoBoxManager.STATE.OVER_SPIDERFY_MARKER);
else
infoBoxManager.turnOn(infoBoxManager.STATE.OVER_MARKER);
infoBoxManager.turnOff(infoBoxManager.STATE.OVER_INFOBOX);
infoBoxManager.turnOff(infoBoxManager.STATE.SHOW_INFOBOX);
infoBoxManager.turnOff(infoBoxManager.STATE.PIN_INFOBOX);
infoBoxManager.setContent(content);
}
// Triggers a resize event and refits the map to the bounds of the markers.
function _refresh() {
google.maps.event.trigger(_mapDOM.map, 'resize');
_mapDOM.map.fitBounds(_markerBounds);
}
export default {
init:init,
renderMapStack:renderMapStack,
toggleSize:toggleSize
};