geofield_map-8.x-2.67/js/geofield_map_gmap_formatter.js
js/geofield_map_gmap_formatter.js
/**
* @file
* Javascript for the Geofield Google Map formatter.
*/
(function ($, Drupal) {
'use strict';
/**
* The Geofield Google Map formatter behavior.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the Geofield Google Map formatter behavior.
*/
Drupal.behaviors.geofieldGoogleMap = {
attach: function (context, drupalSettings) {
function loadMap(mapId) {
let map_settings = drupalSettings['geofield_google_map'][mapId]['map_settings'];
let data = drupalSettings['geofield_google_map'][mapId]['data'];
// Set the map_data[mapid] settings.
Drupal.geoFieldMapFormatter.map_data[mapId] = map_settings;
// Load before the GMap Library, if needed.
Drupal.geoFieldMapFormatter.loadGoogle(mapId, map_settings.gmap_api_key, map_settings.map_additional_libraries, function () {
if (!document.getElementById(mapId)) {
return;
}
Drupal.geoFieldMapFormatter.map_initialize(mapId, map_settings, data, context);
});
}
if (drupalSettings['geofield_google_map']) {
// If the IntersectionObserver API is available, create an observer to load the map when it enters the viewport
// It will be used to handle map loading instead of displaying the map on page load.
let mapObserver = null;
if ('IntersectionObserver' in window){
mapObserver = new IntersectionObserver(function (entries, observer) {
for(var i = 0; i < entries.length; i++) {
if(entries[i].isIntersecting) {
once('geofield-map-loaded', entries[i].target).forEach(function (el) {
loadMap(el.id);
});
}
}
});
}
once('geofield-processed', 'html .geofield-google-map').forEach(function (element) {
const mapId = $(element).attr('id');
if (drupalSettings['geofield_google_map'][mapId]) {
const map_settings = drupalSettings['geofield_google_map'][mapId]['map_settings'];
if (mapObserver && map_settings['map_lazy_load']['lazy_load']) {
mapObserver.observe(element)
} else {
loadMap(mapId);
}
}
});
}
}
};
Drupal.geoFieldMapFormatter = {
map_start: {
center: {lat: 41.85, lng: -87.65},
zoom: 18
},
map_data: {},
// Google Maps are loaded lazily. In some situations load_google() is
// called twice, which results in "You have included the Google Maps API
// multiple times on this page. This may cause unexpected errors." errors.
// This flag will prevent repeat $.getScript() calls.
maps_api_loading: false,
/**
* Returns the re-coded Google Maps api language parameter, from html lang attribute.
*
* @param {string} html_language - The language id string
*
* @return {string} - The transformed language id string
*/
googleMapsLanguage: function (html_language) {
switch (html_language) {
case 'zh-hans':
html_language = 'zh-CN';
break;
case 'zh-hant':
html_language = 'zh-TW';
break;
}
return html_language;
},
/**
* Provides the callback that is called when maps loads.
*/
googleCallback: function () {
let self = this;
// Wait until the window load event to try to use the Maps library.
$(document).ready(function (e) {
self.googleCallbacks.forEach(function (callback) {
callback.callback();
});
self.googleCallbacks = [];
});
},
/**
* Adds a callback that will be called once the maps library is loaded.
*
* @param {string} callback - The callback
*/
addCallback: function (callback) {
let self = this;
// Ensure callbacks array.
self.googleCallbacks = self.googleCallbacks || [];
self.googleCallbacks.push({callback: callback});
},
/**
* Load Google Maps library.
*
* @param {string} mapid - The map id
* @param {string} gmap_api_key - The gmap api key
* @param {array} additional_libraries - Additional Libraries list
* @param {string} callback - the Callback function name
*/
loadGoogle: function (mapid, gmap_api_key, additional_libraries, callback) {
let self = this;
let html_language = $('html').attr('lang') || 'en';
// Add the callback.
self.addCallback(callback);
// Check for Google Maps.
if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
if (self.maps_api_loading === true) {
return;
}
self.maps_api_loading = true;
// Google Maps isn't loaded so lazy load Google Maps.
// Default script path.
let scriptPath = self.map_data[mapid]['gmap_api_localization'] + '?v=weekly&sensor=false&language=' + self.googleMapsLanguage(html_language) + '&callback=Drupal.geoFieldMapFormatter.googleCallback' + '&loading=async';
// If a Google API key is set, use it.
if (gmap_api_key) {
scriptPath += '&key=' + gmap_api_key;
}
if (additional_libraries) {
let libraries = [];
for (let library in additional_libraries) {
if (additional_libraries.hasOwnProperty(library)) {
libraries.push(library);
}
}
scriptPath += '&libraries=' + libraries.join();
}
$.getScript(scriptPath)
.done(function () {
self.maps_api_loading = false;
});
}
else {
// Google Maps loaded. Run callback.
self.googleCallback();
}
},
checkImage: function (imageSrc, setIcon, logError) {
let img = new Image();
img.src = imageSrc;
img.onload = setIcon;
img.onerror = logError;
},
place_feature: function (feature, mapid) {
let self = this;
let icon_image = null;
// Override and set icon image with geojsonProperties.icon, if set as not
// null/empty.
if (feature.geojsonProperties.icon && feature.geojsonProperties.icon.length > 0) {
icon_image = feature.geojsonProperties.icon;
}
// Set the personalized Icon Image, if set.
if (feature.setIcon && icon_image && icon_image.length > 0) {
self.checkImage(icon_image,
// Success loading image.
function () {
feature.setIcon(icon_image);
});
}
// Add a default Tooltip on the title geojsonProperty, if existing.
if (feature.setTitle && feature.geojsonProperties.tooltip) {
feature.setTitle(feature.geojsonProperties.tooltip);
}
const map = self.map_data[mapid].map;
// If the feature is a Point, make it a Marker and extend the Map bounds.
if (feature.getPosition) {
// Define the OverlappingMarkerSpiderfier flag.
const oms = self.map_data[mapid].oms ? self.map_data[mapid].oms : null;
if (oms) {
self.map_data[mapid].oms.addMarker(feature);
}
else {
feature.setMap(map);
}
// Generate the markers object index based on entity id (and geofield
// cardinality), and add the marker to the markers object.
const entity_id = feature['geojsonProperties']['entity_id'];
if (self.map_data[mapid].geofield_cardinality && self.map_data[mapid].geofield_cardinality !== 1) {
let i = 0;
while (self.map_data[mapid].markers[entity_id + '-' + i]) {
i++;
}
self.map_data[mapid].markers[entity_id + '-' + i] = feature;
}
else {
self.map_data[mapid].markers[entity_id] = feature;
}
self.map_data[mapid].map_bounds.extend(feature.getPosition());
// Check for eventual simple or OverlappingMarkerSpiderfier click
// Listener.
const clickListener = oms ? 'spider_click' : 'click';
google.maps.event.addListener(feature, clickListener, function () {
self.infowindow_open(mapid, feature);
});
}
// If the feature is a Polyline or a Polygon, add to the Map and extend
// the Map bounds.
if (feature.getPath) {
let feature_options = feature.geojsonProperties.path_options ? JSON.parse(feature.geojsonProperties.path_options) : {};
feature.setOptions(feature_options);
feature.setMap(map);
let path = feature.getPath();
path.forEach(function (element) {
self.map_data[mapid].map_bounds.extend(element);
});
google.maps.event.addListener(feature, 'click', function (event) {
self.infowindow_open(mapid, feature, event.latLng);
});
}
},
// Closes and open the Map Infowindow at the input feature.
infowindow_open: function (mapid, feature, anchor) {
let self = this;
let map = self.map_data[mapid].map;
let properties = feature.get('geojsonProperties');
if (feature.setTitle && properties && properties.title) {
feature.setTitle(properties.title);
}
map.infowindow.close();
if (properties.description) {
map.infowindow.setContent(properties.description);
// Note: if the feature is a Marker (and not a Polyline/Polygon) its
// extensions will override the infowindow anchor position, in the map.
// infowindow.open method.
map.infowindow.setPosition(anchor);
setTimeout(function () {
map.infowindow.open(map, feature);
}, 200);
}
},
map_refresh: function (mapid) {
let self = this;
setTimeout(function () {
google.maps.event.trigger(self.map_data[mapid].map, 'resize');
}, 10);
},
// Init Geofield Google Map and its functions.
map_initialize: function (mapid, map_settings, data, context) {
let self = this;
// If google and google.maps have been defined.
if (google && google.maps) {
let styledMapType;
let mapOptions = {
center: map_settings.map_center ? new google.maps.LatLng(map_settings.map_center.lat, map_settings.map_center.lon) : new google.maps.LatLng(42, 12.5),
zoom: map_settings.map_zoom_and_pan.zoom.initial ? parseInt(map_settings.map_zoom_and_pan.zoom.initial) : 8,
minZoom: map_settings.map_zoom_and_pan.zoom.min ? parseInt(map_settings.map_zoom_and_pan.zoom.min) : 1,
maxZoom: map_settings.map_zoom_and_pan.zoom.max ? parseInt(map_settings.map_zoom_and_pan.zoom.max) : 20,
gestureHandling: map_settings.map_zoom_and_pan.gestureHandling || 'auto',
mapTypeId: map_settings.map_controls.map_type_id || 'roadmap'
};
// Manage the old scrollwheel & draggable settings (deprecated by Google Maps api).
if (!map_settings.map_zoom_and_pan.scrollwheel && !map_settings.map_zoom_and_pan.gestureHandling) {
mapOptions.scrollwheel = false;
}
if (!map_settings.map_zoom_and_pan.draggable && !map_settings.map_zoom_and_pan.gestureHandling) {
mapOptions.draggable = false;
}
if (map_settings.map_controls.disable_default_ui) {
mapOptions.disableDefaultUI = map_settings.map_controls.disable_default_ui;
}
else {
// Implement Custom Style Map, if Set.
if (map_settings.custom_style_map && map_settings.custom_style_map.custom_style_control && map_settings.custom_style_map.custom_style_name.length > 0 && map_settings.custom_style_map.custom_style_options.length > 0) {
let customMapStyleName = map_settings.custom_style_map.custom_style_name;
let customMapStyle = JSON.parse(map_settings.custom_style_map.custom_style_options);
styledMapType = new google.maps.StyledMapType(customMapStyle, {name: customMapStyleName});
map_settings.map_controls.map_type_control_options_type_ids.push('custom_styled_map');
}
mapOptions.zoomControl = !!map_settings.map_controls.zoom_control;
mapOptions.mapTypeControl = !!map_settings.map_controls.map_type_control;
mapOptions.mapTypeControlOptions = {
mapTypeIds: map_settings.map_controls.map_type_control_options_type_ids ? map_settings.map_controls.map_type_control_options_type_ids : ['roadmap', 'satellite', 'hybrid'],
position: google.maps.ControlPosition.TOP_LEFT
};
mapOptions.scaleControl = !!map_settings.map_controls.scale_control;
mapOptions.streetViewControl = !!map_settings.map_controls.street_view_control;
mapOptions.fullscreenControl = !!map_settings.map_controls.fullscreen_control;
}
// Add map_additional_options if any.
if (map_settings.map_additional_options.length > 0) {
let additionalOptions = JSON.parse(map_settings.map_additional_options);
// Transforms additionalOptions "true", "false" values into true &
// false.
for (let prop in additionalOptions) {
if (additionalOptions.hasOwnProperty(prop)) {
if (additionalOptions[prop] === 'true') {
additionalOptions[prop] = true;
}
if (additionalOptions[prop] === 'false') {
additionalOptions[prop] = false;
}
}
}
// Merge mapOptions with additionalOptions.
$.extend(mapOptions, additionalOptions);
}
// Define the Geofield Google Map.
let map = new google.maps.Map(document.getElementById(mapid), mapOptions);
// Add the Map Reset Control, if set.
if (map_settings.map_zoom_and_pan.map_reset) {
let mapResetControlPosition = map_settings.map_zoom_and_pan.map_reset_position || 'TOP_RIGHT';
// Create the DIV to hold the control and call the mapResetControl()
// constructor passing in this DIV.
let mapResetControlDiv = document.createElement('div');
mapResetControlDiv.style.zIndex = "10";
mapResetControlDiv.index = 1;
new self.map_reset_control(mapResetControlDiv, mapid);
map.controls[google.maps.ControlPosition[mapResetControlPosition]].push(mapResetControlDiv);
}
if (Drupal.geoFieldMapGeocoder && map_settings.map_geocoder.control) {
let mapGeocoderControlPosition = map_settings.map_geocoder.settings.position || 'TOP_RIGHT';
let mapGeocoderControlDiv = document.createElement('div');
Drupal.geoFieldMapFormatter.map_data[mapid].geocoder_control = new Drupal.geoFieldMapGeocoder.map_control(mapGeocoderControlDiv, mapid);
mapGeocoderControlDiv.index = 1;
map.controls[google.maps.ControlPosition[mapGeocoderControlPosition]].push(Drupal.geoFieldMapFormatter.map_data[mapid].geocoder_control);
Drupal.geoFieldMapGeocoder.map_control_autocomplete(mapid, map_settings.map_geocoder.settings, $(Drupal.geoFieldMapFormatter.map_data[mapid].geocoder_control), 'formatter', 'gmap');
}
// If defined a Custom Map Style, associate the styled map with
// custom_styled_map MapTypeId and set it to display.
if (styledMapType) {
map.mapTypes.set('custom_styled_map', styledMapType);
// Set Custom Map Style to Default, if requested.
if (map_settings.custom_style_map && map_settings.custom_style_map.custom_style_default) {
map.setMapTypeId('custom_styled_map');
}
}
// Define a mapid self property, so other code can interact with it.
self.map_data[mapid].map = map;
self.map_data[mapid].map_options = mapOptions;
self.map_data[mapid].features = data.features;
self.map_data[mapid].markers = {};
// Define the MapBounds property.
self.map_data[mapid].map_bounds = new google.maps.LatLngBounds();
// Set the zoom force and center property for the map.
self.map_data[mapid].zoom_force = !!map_settings.map_zoom_and_pan.zoom.force;
self.map_data[mapid].center_force = !!map_settings.map_center.center_force;
// Parse the Geojson data into Google Maps Locations.
let features = [];
for (let i in data.features) {
features[i] = Drupal.googleGeoJson(data.features[i]);
}
if (features.length > 0) {
/**
* Implement OverlappingMarkerSpiderfier if its control set true.
*/
if (map_settings.map_oms && map_settings.map_oms.map_oms_control && OverlappingMarkerSpiderfier) {
let omsOptions = map_settings.map_oms.map_oms_options.length > 0 ? JSON.parse(map_settings.map_oms.map_oms_options) : {
markersWontMove: true,
markersWontHide: true,
basicFormatEvents: true,
keepSpiderfied: true
};
self.map_data[mapid].oms = new OverlappingMarkerSpiderfier(map, omsOptions);
}
map.infowindow = new google.maps.InfoWindow({
content: ''
});
// If the map.infowindow is defined, add an event listener for the
// Ajax Infowindow Popup.
google.maps.event.addListener(map.infowindow, 'domready', function () {
let element = document.createElement('div');
element.innerHTML = map.infowindow.getContent().trim();
let content = $('[data-geofield-google-map-ajax-popup]', element);
if (content.length) {
let url = content.data('geofield-google-map-ajax-popup');
Drupal.ajax({url: url}).execute().done(function () {
Drupal.attachBehaviors(element, drupalSettings);
})
}
});
// Iterate on each features and place it to the map.
if (features.setMap) {
self.place_feature(features, mapid);
}
else {
for (let i in features) {
if (features[i].setMap) {
self.place_feature(features[i], mapid);
}
else {
for (let j in features[i]) {
if (features[i][j].setMap) {
self.place_feature(features[i][j], mapid);
}
}
}
}
}
// Implement Markeclustering, if more than 1 marker on the map,
// and the markercluster option is set to true.
if (typeof MarkerClusterer !== 'undefined' && map_settings.map_markercluster.markercluster_control) {
let markeclusterOption = {
imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'
};
// Add markercluster_additional_options if any.
if (map_settings.map_markercluster.markercluster_additional_options.length > 0) {
const markeclusterAdditionalOptions = JSON.parse(map_settings.map_markercluster.markercluster_additional_options);
// Merge markeclusterOption with markeclusterAdditionalOptions.
$.extend(markeclusterOption, markeclusterAdditionalOptions);
}
// Define a markerCluster property, so other code can interact with
// it.
let markerCluster = [];
const keys = Object.keys(self.map_data[mapid].markers);
for (let k = 0; k < keys.length; k++) {
markerCluster.push(self.map_data[mapid].markers[keys[k]]);
}
self.map_data[mapid].markerCluster = new MarkerClusterer(map, markerCluster, markeclusterOption);
}
// If the Map Initial State is defined by MapBounds,
// and the map center is not forced.
if (!self.mapBoundsAreNull(self.map_data[mapid].map_bounds) && !self.map_data[mapid].center_force) {
map.fitBounds(self.map_data[mapid].map_bounds);
}
// Else if the Map Initial State is defined by Markers in the exact
// same locations,.
else if (self.map_data[mapid].markers.constructor === Object &&
self.mapBoundsAreNull(self.map_data[mapid].map_bounds) &&
// And the map center is not forced.
!self.map_data[mapid].center_force) {
map.setCenter(self.map_data[mapid].markers[Object.keys(self.map_data[mapid].markers)[0]].getPosition());
}
}
google.maps.event.addListenerOnce(map, 'bounds_changed', function () {
// Force the Map Zoom if requested.
if (self.map_data[mapid].zoom_force) {
self.map_data[mapid].map.setZoom(self.map_data[mapid].map_options.zoom);
}
});
// At the beginning (once) ...
google.maps.event.addListenerOnce(map, 'idle', function () {
// Open the Feature infowindow, if so set.
if (self.map_data[mapid].map_marker_and_infowindow.force_open) {
// map.setCenter(features[0].getPosition());
self.infowindow_open(mapid, features[0]);
}
// In case of map initial position not forced, and zooFiner not
// null/neutral, adapt the Map Zoom and the Start Zoom accordingly.
if (!self.map_data[mapid].zoom_force && self.map_data[mapid].map_zoom_and_pan.zoom.finer && self.map_data[mapid].map_zoom_and_pan.zoom.finer !== 0) {
map.setOptions({
zoom: map.getZoom() + parseInt(self.map_data[mapid].map_zoom_and_pan.zoom.finer)
});
}
// Update map initial state after everything is settled.
self.map_set_start_state(mapid, map.getCenter(), map.getZoom());
// Trigger a custom event on Geofield Map initialized, with mapid.
$(context).trigger('geofieldMapInit', mapid);
});
}
},
mapBoundsAreNull: function (mapBounds) {
let north_east = mapBounds.getNorthEast();
let south_west = mapBounds.getSouthWest();
return north_east.toString() === south_west.toString();
},
map_set_start_state: function (mapid, center, zoom) {
let self = this;
self.map_data[mapid].map_start_center = center;
self.map_data[mapid].map_start_zoom = zoom;
},
map_reset_control: function (controlDiv, mapid) {
// Set CSS for the control border.
let controlUI = document.createElement('div');
controlUI.style.backgroundColor = '#fff';
controlUI.style.boxShadow = 'rgba(0,0,0,.3) 0px 1px 4px -1px';
controlUI.style.cursor = 'pointer';
controlUI.title = Drupal.t('Click to reset the map to its initial state');
controlUI.style.margin = '10px';
controlUI.style.position = 'relative';
controlUI.id = 'geofield-map--' + mapid + '--reset-control';
controlDiv.appendChild(controlUI);
// Set CSS for the control interior.
let controlText = document.createElement('div');
controlText.style.position = 'relative';
controlText.innerHTML = Drupal.t('Reset Map');
controlText.style.padding = '0 17px';
controlText.style.display = 'table-cell';
controlText.style.height = '40px';
controlText.style.fontSize = '18px';
controlText.style.color = 'rgb(86,86,86)';
controlText.style.textAlign = 'center';
controlText.style.verticalAlign = 'middle';
controlUI.appendChild(controlText);
// Setup the click event listeners: simply set the map to Chicago.
controlUI.addEventListener('click', function () {
Drupal.geoFieldMapFormatter.map_data[mapid].map.setCenter(Drupal.geoFieldMapFormatter.map_data[mapid].map_start_center);
Drupal.geoFieldMapFormatter.map_data[mapid].map.setZoom(Drupal.geoFieldMapFormatter.map_data[mapid].map_start_zoom);
});
return controlUI;
}
};
})(jQuery, Drupal);
