1/** 2 * @file 3 * Javascript for the Geofield Google Map formatter. 4 */ 5 6(function ($, Drupal) { 7 8 'use strict'; 9 10 /** 11 * The Geofield Google Map formatter behavior. 12 * 13 * @type {Drupal~behavior} 14 * 15 * @prop {Drupal~behaviorAttach} attach 16 * Attaches the Geofield Google Map formatter behavior. 17 */ 18 Drupal.behaviors.geofieldGoogleMap = { 19 attach: function (context, drupalSettings) { 20 21 function loadMap(mapId) { 22 let map_settings = drupalSettings['geofield_google_map'][mapId]['map_settings']; 23 let data = drupalSettings['geofield_google_map'][mapId]['data']; 24 25 // Set the map_data[mapid] settings. 26 Drupal.geoFieldMapFormatter.map_data[mapId] = map_settings; 27 28 // Load before the GMap Library, if needed. 29 Drupal.geoFieldMapFormatter.loadGoogle(mapId, map_settings.gmap_api_key, map_settings.map_additional_libraries, function () { 30 if (!document.getElementById(mapId)) { 31 return; 32 } 33 Drupal.geoFieldMapFormatter.map_initialize(mapId, map_settings, data, context); 34 }); 35 } 36 37 if (drupalSettings['geofield_google_map']) { 38 // If the IntersectionObserver API is available, create an observer to load the map when it enters the viewport 39 // It will be used to handle map loading instead of displaying the map on page load. 40 let mapObserver = null; 41 if ('IntersectionObserver' in window){ 42 mapObserver = new IntersectionObserver(function (entries, observer) { 43 for(var i = 0; i < entries.length; i++) { 44 if(entries[i].isIntersecting) { 45 once('geofield-map-loaded', entries[i].target).forEach(function (el) { 46 loadMap(el.id); 47 }); 48 } 49 } 50 }); 51 } 52 53 once('geofield-processed', 'html .geofield-google-map').forEach(function (element) { 54 const mapId = $(element).attr('id'); 55 if (drupalSettings['geofield_google_map'][mapId]) { 56 const map_settings = drupalSettings['geofield_google_map'][mapId]['map_settings']; 57 if (mapObserver && map_settings['map_lazy_load']['lazy_load']) { 58 mapObserver.observe(element) 59 } else { 60 loadMap(mapId); 61 } 62 } 63 }); 64 } 65 } 66 }; 67 68 Drupal.geoFieldMapFormatter = { 69 70 map_start: { 71 center: {lat: 41.85, lng: -87.65}, 72 zoom: 18 73 }, 74 75 map_data: {}, 76 77 // Google Maps are loaded lazily. In some situations load_google() is 78 // called twice, which results in "You have included the Google Maps API 79 // multiple times on this page. This may cause unexpected errors." errors. 80 // This flag will prevent repeat $.getScript() calls. 81 maps_api_loading: false, 82 83 /** 84 * Returns the re-coded Google Maps api language parameter, from html lang attribute. 85 * 86 * @param {string} html_language - The language id string 87 * 88 * @return {string} - The transformed language id string 89 */ 90 googleMapsLanguage: function (html_language) { 91 switch (html_language) { 92 case 'zh-hans': 93 html_language = 'zh-CN'; 94 break; 95 96 case 'zh-hant': 97 html_language = 'zh-TW'; 98 break; 99 } 100 return html_language; 101 }, 102 103 /** 104 * Provides the callback that is called when maps loads. 105 */ 106 googleCallback: function () { 107 let self = this; 108 // Wait until the window load event to try to use the Maps library. 109 $(document).ready(function (e) { 110 self.googleCallbacks.forEach(function (callback) { 111 callback.callback(); 112 }); 113 self.googleCallbacks = []; 114 }); 115 }, 116 117 /** 118 * Adds a callback that will be called once the maps library is loaded. 119 * 120 * @param {string} callback - The callback 121 */ 122 addCallback: function (callback) { 123 let self = this; 124 // Ensure callbacks array. 125 self.googleCallbacks = self.googleCallbacks || []; 126 self.googleCallbacks.push({callback: callback}); 127 }, 128 129 /** 130 * Load Google Maps library. 131 * 132 * @param {string} mapid - The map id 133 * @param {string} gmap_api_key - The gmap api key 134 * @param {array} additional_libraries - Additional Libraries list 135 * @param {string} callback - the Callback function name 136 */ 137 loadGoogle: function (mapid, gmap_api_key, additional_libraries, callback) { 138 let self = this; 139 let html_language = $('html').attr('lang') || 'en'; 140 141 // Add the callback. 142 self.addCallback(callback); 143 144 // Check for Google Maps. 145 if (typeof google === 'undefined' || typeof google.maps === 'undefined') { 146 if (self.maps_api_loading === true) { 147 return; 148 } 149 150 self.maps_api_loading = true; 151 // Google Maps isn't loaded so lazy load Google Maps. 152 // Default script path. 153 let scriptPath = self.map_data[mapid]['gmap_api_localization'] + '?v=weekly&sensor=false&language=' + self.googleMapsLanguage(html_language) + '&callback=Drupal.geoFieldMapFormatter.googleCallback' + '&loading=async'; 154 155 // If a Google API key is set, use it. 156 if (gmap_api_key) { 157 scriptPath += '&key=' + gmap_api_key; 158 } 159 160 if (additional_libraries) { 161 let libraries = []; 162 for (let library in additional_libraries) { 163 if (additional_libraries.hasOwnProperty(library)) { 164 libraries.push(library); 165 } 166 } 167 scriptPath += '&libraries=' + libraries.join(); 168 } 169 170 $.getScript(scriptPath) 171 .done(function () { 172 self.maps_api_loading = false;
173 }); 174 175 } 176 else { 177 // Google Maps loaded. Run callback. 178 self.googleCallback(); 179 } 180 }, 181 182 checkImage: function (imageSrc, setIcon, logError) { 183 let img = new Image(); 184 img.src = imageSrc; 185 img.onload = setIcon; 186 img.onerror = logError; 187 }, 188 189 place_feature: function (feature, mapid) { 190 let self = this; 191 let icon_image = null; 192 193 // Override and set icon image with geojsonProperties.icon, if set as not 194 // null/empty. 195 if (feature.geojsonProperties.icon && feature.geojsonProperties.icon.length > 0) { 196 icon_image = feature.geojsonProperties.icon; 197 } 198 199 // Set the personalized Icon Image, if set. 200 if (feature.setIcon && icon_image && icon_image.length > 0) { 201 self.checkImage(icon_image, 202 // Success loading image. 203 function () { 204 feature.setIcon(icon_image); 205 }); 206 } 207 208 // Add a default Tooltip on the title geojsonProperty, if existing. 209 if (feature.setTitle && feature.geojsonProperties.tooltip) { 210 feature.setTitle(feature.geojsonProperties.tooltip); 211 } 212 213 const map = self.map_data[mapid].map; 214 215 // If the feature is a Point, make it a Marker and extend the Map bounds. 216 if (feature.getPosition) { 217 // Define the OverlappingMarkerSpiderfier flag. 218 const oms = self.map_data[mapid].oms ? self.map_data[mapid].oms : null; 219 if (oms) { 220 self.map_data[mapid].oms.addMarker(feature); 221 } 222 else { 223 feature.setMap(map); 224 } 225 226 // Generate the markers object index based on entity id (and geofield 227 // cardinality), and add the marker to the markers object. 228 const entity_id = feature['geojsonProperties']['entity_id']; 229 if (self.map_data[mapid].geofield_cardinality && self.map_data[mapid].geofield_cardinality !== 1) { 230 let i = 0; 231 while (self.map_data[mapid].markers[entity_id + '-' + i]) { 232 i++; 233 } 234 self.map_data[mapid].markers[entity_id + '-' + i] = feature; 235 } 236 else { 237 self.map_data[mapid].markers[entity_id] = feature; 238 } 239 240 self.map_data[mapid].map_bounds.extend(feature.getPosition()); 241 242 // Check for eventual simple or OverlappingMarkerSpiderfier click 243 // Listener. 244 const clickListener = oms ? 'spider_click' : 'click'; 245 google.maps.event.addListener(feature, clickListener, function () { 246 self.infowindow_open(mapid, feature); 247 }); 248 } 249 250 // If the feature is a Polyline or a Polygon, add to the Map and extend 251 // the Map bounds. 252 if (feature.getPath) { 253 let feature_options = feature.geojsonProperties.path_options ? JSON.parse(feature.geojsonProperties.path_options) : {}; 254 feature.setOptions(feature_options); 255 feature.setMap(map); 256 let path = feature.getPath(); 257 path.forEach(function (element) { 258 self.map_data[mapid].map_bounds.extend(element); 259 }); 260 google.maps.event.addListener(feature, 'click', function (event) { 261 self.infowindow_open(mapid, feature, event.latLng); 262 }); 263 } 264 265 }, 266 267 // Closes and open the Map Infowindow at the input feature. 268 infowindow_open: function (mapid, feature, anchor) { 269 let self = this; 270 let map = self.map_data[mapid].map; 271 let properties = feature.get('geojsonProperties'); 272 if (feature.setTitle && properties && properties.title) { 273 feature.setTitle(properties.title); 274 } 275 map.infowindow.close(); 276 if (properties.description) { 277 map.infowindow.setContent(properties.description); 278 279 // Note: if the feature is a Marker (and not a Polyline/Polygon) its 280 // extensions will override the infowindow anchor position, in the map. 281 // infowindow.open method. 282 map.infowindow.setPosition(anchor); 283 setTimeout(function () { 284 map.infowindow.open(map, feature); 285 }, 200); 286 } 287 }, 288 289 map_refresh: function (mapid) { 290 let self = this; 291 setTimeout(function () { 292 google.maps.event.trigger(self.map_data[mapid].map, 'resize'); 293 }, 10); 294 }, 295 296 // Init Geofield Google Map and its functions. 297 map_initialize: function (mapid, map_settings, data, context) { 298 let self = this; 299 300 // If google and google.maps have been defined. 301 if (google && google.maps) { 302 let styledMapType; 303 304 let mapOptions = { 305 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), 306 zoom: map_settings.map_zoom_and_pan.zoom.initial ? parseInt(map_settings.map_zoom_and_pan.zoom.initial) : 8, 307 minZoom: map_settings.map_zoom_and_pan.zoom.min ? parseInt(map_settings.map_zoom_and_pan.zoom.min) : 1, 308 maxZoom: map_settings.map_zoom_and_pan.zoom.max ? parseInt(map_settings.map_zoom_and_pan.zoom.max) : 20, 309 gestureHandling: map_settings.map_zoom_and_pan.gestureHandling || 'auto', 310 mapTypeId: map_settings.map_controls.map_type_id || 'roadmap' 311 }; 312 313 // Manage the old scrollwheel & draggable settings (deprecated by Google Maps api). 314 if (!map_settings.map_zoom_and_pan.scrollwheel && !map_settings.map_zoom_and_pan.gestureHandling) { 315 mapOptions.scrollwheel = false;
316 } 317 if (!map_settings.map_zoom_and_pan.draggable && !map_settings.map_zoom_and_pan.gestureHandling) { 318 mapOptions.draggable = false; 319 } 320 321 if (map_settings.map_controls.disable_default_ui) { 322 mapOptions.disableDefaultUI = map_settings.map_controls.disable_default_ui; 323 } 324 else { 325 // Implement Custom Style Map, if Set. 326 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) { 327 let customMapStyleName = map_settings.custom_style_map.custom_style_name; 328 let customMapStyle = JSON.parse(map_settings.custom_style_map.custom_style_options); 329 styledMapType = new google.maps.StyledMapType(customMapStyle, {name: customMapStyleName}); 330 map_settings.map_controls.map_type_control_options_type_ids.push('custom_styled_map'); 331 } 332 333 mapOptions.zoomControl = !!map_settings.map_controls.zoom_control; 334 mapOptions.mapTypeControl = !!map_settings.map_controls.map_type_control; 335 mapOptions.mapTypeControlOptions = { 336 mapTypeIds: map_settings.map_controls.map_type_control_options_type_ids ? map_settings.map_controls.map_type_control_options_type_ids : ['roadmap', 'satellite', 'hybrid'], 337 position: google.maps.ControlPosition.TOP_LEFT 338 }; 339 mapOptions.scaleControl = !!map_settings.map_controls.scale_control; 340 mapOptions.streetViewControl = !!map_settings.map_controls.street_view_control; 341 mapOptions.fullscreenControl = !!map_settings.map_controls.fullscreen_control; 342 } 343 344 // Add map_additional_options if any. 345 if (map_settings.map_additional_options.length > 0) { 346 let additionalOptions = JSON.parse(map_settings.map_additional_options); 347 // Transforms additionalOptions "true", "false" values into true & 348 // false. 349 for (let prop in additionalOptions) { 350 if (additionalOptions.hasOwnProperty(prop)) { 351 if (additionalOptions[prop] === 'true') { 352 additionalOptions[prop] = true; 353 } 354 if (additionalOptions[prop] === 'false') { 355 additionalOptions[prop] = false; 356 } 357 } 358 } 359 // Merge mapOptions with additionalOptions. 360 $.extend(mapOptions, additionalOptions); 361 } 362 363 // Define the Geofield Google Map. 364 let map = new google.maps.Map(document.getElementById(mapid), mapOptions); 365 366 // Add the Map Reset Control, if set. 367 if (map_settings.map_zoom_and_pan.map_reset) { 368 let mapResetControlPosition = map_settings.map_zoom_and_pan.map_reset_position || 'TOP_RIGHT'; 369 370 // Create the DIV to hold the control and call the mapResetControl() 371 // constructor passing in this DIV. 372 let mapResetControlDiv = document.createElement('div'); 373 mapResetControlDiv.style.zIndex = "10"; 374 mapResetControlDiv.index = 1; 375 new self.map_reset_control(mapResetControlDiv, mapid); 376 map.controls[google.maps.ControlPosition[mapResetControlPosition]].push(mapResetControlDiv); 377 } 378 379 if (Drupal.geoFieldMapGeocoder && map_settings.map_geocoder.control) { 380 let mapGeocoderControlPosition = map_settings.map_geocoder.settings.position || 'TOP_RIGHT'; 381 let mapGeocoderControlDiv = document.createElement('div'); 382 Drupal.geoFieldMapFormatter.map_data[mapid].geocoder_control = new Drupal.geoFieldMapGeocoder.map_control(mapGeocoderControlDiv, mapid); 383 mapGeocoderControlDiv.index = 1; 384 map.controls[google.maps.ControlPosition[mapGeocoderControlPosition]].push(Drupal.geoFieldMapFormatter.map_data[mapid].geocoder_control); 385 Drupal.geoFieldMapGeocoder.map_control_autocomplete(mapid, map_settings.map_geocoder.settings, $(Drupal.geoFieldMapFormatter.map_data[mapid].geocoder_control), 'formatter', 'gmap'); 386 } 387 388 // If defined a Custom Map Style, associate the styled map with 389 // custom_styled_map MapTypeId and set it to display. 390 if (styledMapType) { 391 map.mapTypes.set('custom_styled_map', styledMapType); 392 // Set Custom Map Style to Default, if requested. 393 if (map_settings.custom_style_map && map_settings.custom_style_map.custom_style_default) { 394 map.setMapTypeId('custom_styled_map'); 395 } 396 } 397
398 // Define a mapid self property, so other code can interact with it. 399 self.map_data[mapid].map = map; 400 self.map_data[mapid].map_options = mapOptions; 401 self.map_data[mapid].features = data.features; 402 self.map_data[mapid].markers = {}; 403 404 // Define the MapBounds property. 405 self.map_data[mapid].map_bounds = new google.maps.LatLngBounds(); 406 407 // Set the zoom force and center property for the map. 408 self.map_data[mapid].zoom_force = !!map_settings.map_zoom_and_pan.zoom.force; 409 self.map_data[mapid].center_force = !!map_settings.map_center.center_force; 410 411 // Parse the Geojson data into Google Maps Locations. 412 let features = []; 413 for (let i in data.features) { 414 features[i] = Drupal.googleGeoJson(data.features[i]); 415 } 416 417 if (features.length > 0) { 418 419 /** 420 * Implement OverlappingMarkerSpiderfier if its control set true. 421 */ 422 if (map_settings.map_oms && map_settings.map_oms.map_oms_control && OverlappingMarkerSpiderfier) { 423 let omsOptions = map_settings.map_oms.map_oms_options.length > 0 ? JSON.parse(map_settings.map_oms.map_oms_options) : { 424 markersWontMove: true, 425 markersWontHide: true, 426 basicFormatEvents: true, 427 keepSpiderfied: true 428 }; 429 self.map_data[mapid].oms = new OverlappingMarkerSpiderfier(map, omsOptions); 430 } 431 432 map.infowindow = new google.maps.InfoWindow({ 433 content: '' 434 }); 435 436 // If the map.infowindow is defined, add an event listener for the 437 // Ajax Infowindow Popup. 438 google.maps.event.addListener(map.infowindow, 'domready', function () { 439 let element = document.createElement('div'); 440 element.innerHTML = map.infowindow.getContent().trim(); 441 let content = $('[data-geofield-google-map-ajax-popup]', element); 442 if (content.length) { 443 let url = content.data('geofield-google-map-ajax-popup'); 444 Drupal.ajax({url: url}).execute().done(function () { 445 Drupal.attachBehaviors(element, drupalSettings); 446 }) 447 } 448 }); 449 450 // Iterate on each features and place it to the map. 451 if (features.setMap) { 452 self.place_feature(features, mapid); 453 } 454 else { 455 for (let i in features) { 456 if (features[i].setMap) { 457 self.place_feature(features[i], mapid); 458 } 459 else { 460 for (let j in features[i]) { 461 if (features[i][j].setMap) { 462 self.place_feature(features[i][j], mapid); 463 } 464 } 465 } 466 } 467 } 468 469 // Implement Markeclustering, if more than 1 marker on the map, 470 // and the markercluster option is set to true. 471 if (typeof MarkerClusterer !== 'undefined' && map_settings.map_markercluster.markercluster_control) { 472 473 let markeclusterOption = { 474 imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m' 475 }; 476 477 // Add markercluster_additional_options if any. 478 if (map_settings.map_markercluster.markercluster_additional_options.length > 0) { 479 const markeclusterAdditionalOptions = JSON.parse(map_settings.map_markercluster.markercluster_additional_options); 480 // Merge markeclusterOption with markeclusterAdditionalOptions. 481 $.extend(markeclusterOption, markeclusterAdditionalOptions); 482 } 483 484 // Define a markerCluster property, so other code can interact with 485 // it. 486 let markerCluster = []; 487 const keys = Object.keys(self.map_data[mapid].markers); 488 for (let k = 0; k < keys.length; k++) { 489 markerCluster.push(self.map_data[mapid].markers[keys[k]]); 490 } 491 self.map_data[mapid].markerCluster = new MarkerClusterer(map, markerCluster, markeclusterOption); 492 } 493 494 // If the Map Initial State is defined by MapBounds, 495 // and the map center is not forced. 496 if (!self.mapBoundsAreNull(self.map_data[mapid].map_bounds) && !self.map_data[mapid].center_force) { 497 map.fitBounds(self.map_data[mapid].map_bounds); 498 } 499 // Else if the Map Initial State is defined by Markers in the exact 500 // same locations,. 501 else if (self.map_data[mapid].markers.constructor === Object && 502 self.mapBoundsAreNull(self.map_data[mapid].map_bounds) && 503 // And the map center is not forced. 504 !self.map_data[mapid].center_force) { 505 map.setCenter(self.map_data[mapid].markers[Object.keys(self.map_data[mapid].markers)[0]].getPosition()); 506 } 507 508 } 509 510 google.maps.event.addListenerOnce(map, 'bounds_changed', function () { 511 // Force the Map Zoom if requested. 512 if (self.map_data[mapid].zoom_force) { 513 self.map_data[mapid].map.setZoom(self.map_data[mapid].map_options.zoom); 514 } 515 }); 516 517 // At the beginning (once) ... 518 google.maps.event.addListenerOnce(map, 'idle', function () { 519 520 // Open the Feature infowindow, if so set. 521 if (self.map_data[mapid].map_marker_and_infowindow.force_open) { 522 // map.setCenter(features[0].getPosition()); 523 self.infowindow_open(mapid, features[0]); 524 } 525 526 // In case of map initial position not forced, and zooFiner not 527 // null/neutral, adapt the Map Zoom and the Start Zoom accordingly. 528 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) { 529 map.setOptions({ 530 zoom: map.getZoom() + parseInt(self.map_data[mapid].map_zoom_and_pan.zoom.finer) 531 }); 532 } 533 534 // Update map initial state after everything is settled. 535 self.map_set_start_state(mapid, map.getCenter(), map.getZoom()); 536 537 // Trigger a custom event on Geofield Map initialized, with mapid. 538 $(context).trigger('geofieldMapInit', mapid); 539 }); 540 541 } 542 }, 543 544 mapBoundsAreNull: function (mapBounds) { 545 let north_east = mapBounds.getNorthEast();
546 let south_west = mapBounds.getSouthWest(); 547 return north_east.toString() === south_west.toString(); 548 549 }, 550 551 map_set_start_state: function (mapid, center, zoom) { 552 let self = this; 553 self.map_data[mapid].map_start_center = center; 554 self.map_data[mapid].map_start_zoom = zoom; 555 }, 556 557 map_reset_control: function (controlDiv, mapid) { 558 // Set CSS for the control border. 559 let controlUI = document.createElement('div'); 560 controlUI.style.backgroundColor = '#fff'; 561 controlUI.style.boxShadow = 'rgba(0,0,0,.3) 0px 1px 4px -1px'; 562 controlUI.style.cursor = 'pointer'; 563 controlUI.title = Drupal.t('Click to reset the map to its initial state'); 564 controlUI.style.margin = '10px'; 565 controlUI.style.position = 'relative'; 566 controlUI.id = 'geofield-map--' + mapid + '--reset-control'; 567 controlDiv.appendChild(controlUI); 568 569 // Set CSS for the control interior. 570 let controlText = document.createElement('div'); 571 controlText.style.position = 'relative'; 572 controlText.innerHTML = Drupal.t('Reset Map'); 573 controlText.style.padding = '0 17px'; 574 controlText.style.display = 'table-cell'; 575 controlText.style.height = '40px'; 576 controlText.style.fontSize = '18px'; 577 controlText.style.color = 'rgb(86,86,86)'; 578 controlText.style.textAlign = 'center'; 579 controlText.style.verticalAlign = 'middle'; 580 controlUI.appendChild(controlText); 581 582 // Setup the click event listeners: simply set the map to Chicago. 583 controlUI.addEventListener('click', function () { 584 Drupal.geoFieldMapFormatter.map_data[mapid].map.setCenter(Drupal.geoFieldMapFormatter.map_data[mapid].map_start_center); 585 Drupal.geoFieldMapFormatter.map_data[mapid].map.setZoom(Drupal.geoFieldMapFormatter.map_data[mapid].map_start_zoom); 586 }); 587 return controlUI; 588 } 589 590 }; 591 592})(jQuery, Drupal);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.