1/* eslint-disable */ 2 3var WPViews = WPViews || {}; 4 5WPViews.ViewAddonMaps = function( $ ) { 6 window.toolset_maps_config = window.toolset_maps_config || {}; 7 8 const API_GOOGLE = 'google'; 9 const API_AZURE = 'azure'; 10 const API_OSM = 'osm'; 11 12 const self = this; 13 14 /** @var {Object} toolset_maps_config */ 15 16 // Determine which API is in use and set self.api as needed 17 if ( API_GOOGLE === window.toolset_maps_config.provider ) { 18 if ( typeof google === 'undefined' || typeof google.maps === 'undefined' ) { 19 self.api = null; 20 } else { 21 /** @var {Object} google */ 22 self.api = google.maps; 23 } 24 } else { 25 /** @var {Object} Azure or OSM: We donât set self.api immediately, we lazy-load or initialize late */ 26 self.api = null; 27 } 28 29 self.maps_data = []; 30 self.maps = {}; 31 self.markers = {}; 32 self.markerGroups = {}; 33 self.infowindows = {}; 34 self.bounds = {}; 35 self.view_map_blocks_relation = {}; 36 37 self.markerInteractions = {}; 38 self.azurePinContexts = {}; 39 40 self.default_cluster_options = { 41 imagePath: window.toolset_maps_config.clusterImagePath, 42 gridSize: 60, 43 maxZoom: null, 44 zoomOnClick: true, 45 minimumClusterSize: 2 46 }; 47 48 self.cluster_options = {}; 49 self.has_cluster_options = {}; 50 51 self.resize_queue = []; 52 53 /** 54 * collect_maps_data 55 * 56 * Before init_maps 57 * 58 * @since 1.0 59 */ 60 61 self.collect_maps_data = function() { 62 var mapElements = $( '.js-wpv-addon-maps-render' ); 63 if ( mapElements.length === 0 ) { 64 // No map elements found, data cannot be collected 65 return; 66 } 67 68 $( '.js-wpv-addon-maps-render' ).each( function() { 69 self.collect_map_data( $( this ) ); 70 }); 71 72 $( document ).trigger('js_event_wpv_addon_maps_map_data_collected'); 73 }; 74 75 /** 76 * Initializes street view mode on given map. 77 * @param Object map 78 * @param latlon 79 * @since 1.5 80 */ 81 self.initStreetView = function( map, latlon ) { 82 var streetViewService = new google.maps.StreetViewService(); 83 var panorama = self.maps[ map.map_id ].getStreetView(); 84 85 streetViewService.getPanoramaByLocation( latlon, 100, function (streetViewPanoramaData, status) { 86 87 if ( status === google.maps.StreetViewStatus.OK ) { 88 var streetLatlon = streetViewPanoramaData.location.latLng; 89 var heading = ( map.map_options.heading !== '' ) 90 ? map.map_options.heading 91 : google.maps.geometry.spherical.computeHeading( streetLatlon, latlon); 92 var pitch = ( map.map_options.pitch !== '' ) ? map.map_options.pitch : 0; 93 94 panorama.setPosition( streetLatlon ); 95 panorama.setPov({ 96 heading: heading, 97 pitch: pitch 98 }); 99 panorama.setVisible( true ); 100 } 101 }); 102 }; 103 104 /** 105 * Sets a trigger to init street view after map is ready. Namespaces trigger so we do this only once per map. 106 * @param String mapId 107 * @param jQuery $marker 108 * @listens js_event_wpv_addon_maps_init_map_completed 109 * @since 1.5 110 */ 111 self.waitForMapInitThenInitStreetView = function( mapId, $marker ) { 112 $( document ).on( 'js_event_wpv_addon_maps_init_map_completed', function( event, map ) { 113 if ( mapId === map.map_id ){ 114 var latlon = new google.maps.LatLng( $marker.data('markerlat'), $marker.data('markerlon') ); 115 self.initStreetView( map, latlon ); 116 } 117 } ); 118 }; 119 120 /** 121 * collect_map_data 122 * 123 * @since 1.0 124 * @since 1.5 handle Street Views 125 */ 126 self.collect_map_data = function( thiz_map, attributes ) { 127 var thiz_map_id, 128 thiz_map_points = [], 129 thiz_map_options = {}; 130 var streetViewMarkerFound = false; 131 132 if ( attributes ) { 133 // Use attributes to build map data 134 thiz_map_id = attributes.mapId; 135 thiz_map_options['general_zoom'] = parseInt( attributes.mapZoomLevel ) || 5; 136 thiz_map_options['general_center_lat'] = parseFloat( attributes.mapCenterLat ) || 0; 137 thiz_map_options['general_center_lon'] = parseFloat( attributes.mapCenterLon ) || 0; 138 thiz_map_options['fitbounds'] = attributes.mapZoomAutomatic ? 'on' : 'off'; 139 thiz_map_options['single_zoom'] = parseInt( attributes.mapZoomLevelForSingleMarker ) || thiz_map_options['general_zoom']; 140 thiz_map_options['single_center'] = attributes.mapForceCenterSettingForSingleMarker ? 'on' : 'off'; 141 thiz_map_options['osm_layer'] = attributes.osmTileLayer || 'standard'; 142 thiz_map_options['map_type'] = attributes.mapType || 'roadmap'; 143 thiz_map_options['show_layer_interests'] = attributes.showLayerInterests || '';
144 thiz_map_options['marker_icon'] = attributes.mapMarkerIcon || ''; 145 thiz_map_options['marker_icon_hover'] = attributes.mapMarkerIconHover || ''; 146 thiz_map_options['draggable'] = attributes.mapDraggable ? 'on' : 'off'; 147 thiz_map_options['scrollwheel'] = attributes.mapScrollable ? 'on' : 'off'; 148 thiz_map_options['double_click_zoom'] = attributes.mapDoubleClickZoom ? 'on' : 'off'; 149 thiz_map_options['map_type_control'] = attributes.mapTypeControl ? 'on' : 'off'; 150 thiz_map_options['full_screen_control'] = attributes.mapFullScreenControl ? 'on' : 'off'; 151 thiz_map_options['zoom_control'] = attributes.mapZoomControls ? 'on' : 'off'; 152 thiz_map_options['street_view_control'] = attributes.mapStreetViewControl ? 'on' : 'off'; 153 thiz_map_options['background_color'] = attributes.mapBackgroundColor || ''; 154 thiz_map_options['cluster'] = attributes.mapMarkerClustering ? 'on' : 'off'; 155 thiz_map_options['style_json'] = attributes.mapStyle || ''; 156 thiz_map_options['spiderfy'] = attributes.mapMarkerSpiderfying ? 'on' : 'off'; 157 thiz_map_options['lat'] = attributes.mapCenterLat ? parseFloat( attributes.mapCenterLat ) : null; 158 thiz_map_options['long'] = attributes.mapCenterLon ? parseFloat( attributes.mapCenterLon ) : null; 159 thiz_map_options['heading'] = parseFloat( attributes.heading ) || 0; 160 thiz_map_options['pitch'] = parseFloat( attributes.pitch ) || 0; 161 thiz_map_options['multiple_zoom'] = parseInt( attributes.mapZoomLevelForMultipleMarkers ) || thiz_map_options['general_zoom']; 162 thiz_map_options['clusterclickzoom'] = attributes.mapMarkerClusteringClickZoom ? 'on' : 'off'; 163 thiz_map_options['clustermaxzoom'] = parseInt( attributes.mapMarkerClusteringMaximalZoomLevel ) || null; 164 thiz_map_options['clustergridsize'] = parseInt( attributes.mapMarkerClusteringMinimalDistance ) || 60; 165 thiz_map_options['clusterminsize'] = parseInt( attributes.mapMarkerClusteringMinimalNumber ) || 2; 166 thiz_map_options['marker_icon_use_hover'] = attributes.mapMarkerIconUseDifferentForHover ? 'on' : 'off'; 167 thiz_map_options['street_view'] = attributes.mapStreetView ? 'on' : 'off'; 168 thiz_map_options['loading_text'] = attributes.mapLoadingText || ''; 169 thiz_map_options['width'] = attributes.mapWidth || '100%'; 170 thiz_map_options['height'] = attributes.mapHeight || '500px'; 171 172 } else { 173 // Original code using DOM elements 174 thiz_map_id = thiz_map.data( 'map' ); 175 thiz_map_options['general_zoom'] = thiz_map.data( 'generalzoom' ); 176 thiz_map_options['general_center_lat'] = thiz_map.data( 'generalcenterlat' ); 177 thiz_map_options['general_center_lon'] = thiz_map.data( 'generalcenterlon' ); 178 thiz_map_options['fitbounds'] = thiz_map.data( 'fitbounds' ); 179 thiz_map_options['single_zoom'] = thiz_map.data( 'singlezoom' ); 180 thiz_map_options['single_center'] = thiz_map.data( 'singlecenter' ); 181 thiz_map_options['osm_layer'] = thiz_map.data( 'osmlayer' ); 182 thiz_map_options['map_type'] = thiz_map.data( 'maptype' ); 183 thiz_map_options['show_layer_interests'] = thiz_map.data( 'showlayerinterests' ); 184 thiz_map_options['marker_icon'] = thiz_map.data( 'markericon' ); 185 thiz_map_options['marker_icon_hover'] = thiz_map.data( 'markericonhover' ); 186 var marker_icon_use_hover = thiz_map.data( 'markericonusehover' ); 187 thiz_map_options['marker_icon_use_hover'] = 'undefined' === typeof marker_icon_use_hover 188 ? 'on' 189 : marker_icon_use_hover; 190 thiz_map_options['draggable'] = thiz_map.data( 'draggable' ); 191 thiz_map_options['scrollwheel'] = thiz_map.data( 'scrollwheel' ); 192 thiz_map_options['double_click_zoom'] = thiz_map.data( 'doubleclickzoom' ); 193 thiz_map_options['map_type_control'] = thiz_map.data( 'maptypecontrol' ); 194 thiz_map_options['full_screen_control'] = thiz_map.data( 'fullscreencontrol' ); 195 thiz_map_options['zoom_control'] = thiz_map.data( 'zoomcontrol' ); 196 thiz_map_options['street_view_control'] = thiz_map.data( 'streetviewcontrol' ); 197 thiz_map_options['background_color'] = thiz_map.data( 'backgroundcolor' ); 198 thiz_map_options['cluster'] = thiz_map.data( 'cluster' ); 199 thiz_map_options['clustergridsize'] = thiz_map.data( 'clustergridsize' ); 200 thiz_map_options['clustermaxzoom'] = thiz_map.data( 'clustermaxzoom' ); 201 thiz_map_options['clusterclickzoom'] = thiz_map.data( 'clusterclickzoom' ); 202 thiz_map_options['clusterminsize'] = thiz_map.data( 'clusterminsize' ); 203 thiz_map_options['style_json'] = thiz_map.data( 'stylejson' ); 204 thiz_map_options['spiderfy'] = thiz_map.data( 'spiderfy' ); 205 thiz_map_options['lat'] = thiz_map.data( 'lat' ); 206 thiz_map_options['long'] = thiz_map.data( 'long' ); 207 thiz_map_options['heading'] = thiz_map.data( 'heading' ); 208 thiz_map_options['pitch'] = thiz_map.data( 'pitch' ); 209 } 210 211 $( '.js-wpv-addon-maps-markerfor-' + thiz_map_id ).each( function() { 212 var thiz_marker = $( this ); 213 // Handle special case when we don't have coordinates, but instead need to ask browser for current user's 214 // position (only for Google API) 215 if ( 216 thiz_marker.data( 'markerlat' ) === 'geo' 217 && API_GOOGLE === window.toolset_maps_config.provider 218 ) { 219 // In case map render wait is requested by a marker, add that data to map 220 if ( thiz_marker.data( 'markerlon' ) === 'wait' ) { 221 thiz_map_options['render'] = 'wait'; 222 self.add_current_visitor_location_after_geolocation( thiz_map_id, thiz_marker ); 223 } else { 224 self.add_current_visitor_location_after_init( thiz_map_id, thiz_marker ); 225 } 226 return true; 227 } 228 229 // Handle Street View marker as special case (only when using Google API) 230 if ( 231 thiz_map.data( 'streetview' ) === 'on' 232 && API_GOOGLE === window.toolset_maps_config.provider 233 ) { 234 if ( thiz_map.data( 'markerid' ) === thiz_marker.data( 'marker' ) ) {
235 self.waitForMapInitThenInitStreetView( thiz_map_id, thiz_marker ); 236 streetViewMarkerFound = true; 237 return true; 238 } else if ( thiz_map.data( 'location' ) === 'first' && !streetViewMarkerFound ) { 239 self.waitForMapInitThenInitStreetView( thiz_map_id, thiz_marker ); 240 streetViewMarkerFound = true; 241 return true; 242 } 243 } 244 245 thiz_map_points.push( { 246 'marker': thiz_marker.data( 'marker' ), 247 'title': self.getPlainTextTitle( thiz_marker.attr( 'data-markertitle' ) ), 248 'markerlat': thiz_marker.data( 'markerlat' ), 249 'markerlon': thiz_marker.data( 'markerlon' ), 250 'markerinfowindow': thiz_marker.html(), 251 'markericon': thiz_marker.data( 'markericon' ), 252 'markericonhover': thiz_marker.data( 'markericonhover' ) 253 } ); 254 } ); 255 256 // Some error catching when street view from marker requested and marker not found 257 if ( 258 ( 259 thiz_map.data( 'location' ) === 'first' 260 || thiz_map.data( 'markerid' ) 261 ) 262 && !streetViewMarkerFound 263 ) { 264 console.warn( views_addon_maps_i10n.marker_not_found_warning, thiz_map_id ); 265 } 266 267 var thiz_cluster_options = { 268 'cluster': thiz_map_options['cluster'], 269 'gridSize': parseInt( thiz_map_options['clustergridsize'] ), 270 'maxZoom': ( parseInt( thiz_map_options['clustermaxzoom'] ) > 0 ) ? parseInt( thiz_map_options['clustermaxzoom'] ) : null, 271 'zoomOnClick': ( thiz_map_options['clusterclickzoom'] == 'off' ) ? false : true, 272 'minimumClusterSize': parseInt( thiz_map_options['clusterminsize'] ) 273 }; 274 275 // As we might have cleared those options if we are on a reload event, we need to set them again with the data we saved in self.has_cluster_options 276 if ( _.has( self.has_cluster_options, thiz_map_id ) ) { 277 if ( _.has( self.has_cluster_options[ thiz_map_id ], "styles" ) ) { 278 thiz_cluster_options['styles'] = self.has_cluster_options[ thiz_map_id ]['styles']; 279 } 280 if ( _.has( self.has_cluster_options[ thiz_map_id ], "calculator" ) ) { 281 thiz_cluster_options['calculator'] = self.has_cluster_options[ thiz_map_id ]['calculator']; 282 } 283 } 284 285 var thiz_map_collected = { 286 'map': thiz_map_id, 287 'markers': thiz_map_points, 288 'options': thiz_map_options, 289 'cluster_options': thiz_cluster_options 290 }; 291 292 self.maps_data.push( thiz_map_collected ); 293 self.cluster_options[ thiz_map_id ] = thiz_cluster_options; 294 295 return thiz_map_collected; 296 }; 297 298 /** 299 * Gets current visitor location from browser, then adds the marker to given map. 300 * 301 * If location fetching failed, render the map without it... 302 * 303 * @since 1.4 304 * @param {String} thiz_map_id 305 * @param {jQuery} thiz_marker 306 */ 307 self.add_current_visitor_location = function(thiz_map_id, thiz_marker) { 308 var map_key = self.get_map_key_by_id(thiz_map_id); 309 if (navigator.geolocation) { 310 navigator.geolocation.getCurrentPosition( 311 function (position) { 312 // Add data to collection and ask map to redraw 313 self.maps_data[map_key].markers.push({ 314 'marker': thiz_marker.data('marker'), 315 'title': self.getPlainTextTitle( thiz_marker.attr( 'data-markertitle' ) ), 316 'markerlat': position.coords.latitude, 317 'markerlon': position.coords.longitude, 318 'markerinfowindow': thiz_marker.html(), 319 'markericon': thiz_marker.data('markericon'), 320 'markericonhover': thiz_marker.data('markericonhover') 321 }); 322 323 self.init_map_after_loading_styles(self.maps_data[map_key]); 324 }, 325 function (position_error) { 326 self.init_map_after_loading_styles(self.maps_data[map_key]); 327 } 328 ); 329 } else { 330 self.init_map_after_loading_styles(self.maps_data[map_key]); 331 } 332 }; 333 334 /** 335 * Wraps waiting for map to render first time before adding browser location marker 336 * @since 1.4 337 * @param {String} thiz_map_id 338 * @param {jQuery} thiz_marker 339 */ 340 self.add_current_visitor_location_after_init = function(thiz_map_id, thiz_marker) { 341 $( document ).on( 'js_event_wpv_addon_maps_init_map_completed.'+thiz_map_id, function( event, data ) { 342 if (thiz_map_id === data.map_id) { 343 // Stop listening to this event for this map (because the event can fire multiple times 344 // and we really need to add marker only once) 345 $( document ).off( 'js_event_wpv_addon_maps_init_map_completed.'+thiz_map_id ); 346 347 self.add_current_visitor_location(thiz_map_id, thiz_marker); 348 } 349 }); 350 }; 351 352 /** 353 * Wraps waiting for geolocation before rendering map. 354 * 355 * Waits for all map data to be ready before trying to render it. (Problems could otherwise happen when geolocation 356 * is already approved and map data is not yet collected.) 357 * 358 * @since 1.4 359 * @param {String} thiz_map_id 360 * @param {jQuery} thiz_marker 361 */ 362 self.add_current_visitor_location_after_geolocation = function(thiz_map_id, thiz_marker) { 363 $( document ).one('js_event_wpv_addon_maps_map_data_collected.'+thiz_map_id, function() { 364 self.add_current_visitor_location(thiz_map_id, thiz_marker); 365 }); 366 }; 367 368 /** 369 * Given map id string returns array key for a map in maps_data 370 * @since 1.4 371 * @param {String} map_id 372 * @returns {Number} 373 */
374 self.get_map_key_by_id = function(map_id) { 375 return _.findLastIndex(self.maps_data, { map: map_id }); 376 }; 377 378 /** 379 * Checks if json style needed, loads JSON file with map styles and then inits the map 380 * @since 1.4 381 * @param {Object} map 382 */ 383 self.init_map_after_loading_styles = function( map ) { 384 if ( map.options.style_json ) { 385 $.getJSON( map.options.style_json ) 386 .done( function( styles ) { 387 map.options.styles = styles; 388 } ) 389 .always( function () { 390 // Even if styles loading failed, map can be rendered with standard style, so do it always. 391 self.initMapOrWaitIfInsideHiddenBootstrapAccordionOrTab( map ); 392 } ); 393 } else { 394 self.initMapOrWaitIfInsideHiddenBootstrapAccordionOrTab( map ); 395 } 396 }; 397 398 /** 399 * Checks if this map is inside a hidden (collapsed) Bootstrap accordion. 400 * 401 * @param {String} map_selector 402 * 403 * @return {String} Returns accordion id if found, empty string otherwise 404 * 405 * @since 1.4.2 406 */ 407 self.check_being_inside_collapsed_bootstrap_accordion = function( map_selector ) { 408 var accordion_id = ''; 409 var toggle_href = ''; 410 411 // Find all accordion toggles which are currently collapsed, then find which of the accordions contains our map 412 // and return accordion id. If none, we'll return empty string. 413 $('a[data-toggle="collapse"].collapsed').each( function( index, toggle ) { 414 toggle_href = $( toggle ).attr('href'); 415 416 if ( $( toggle_href ).has( map_selector ).length ) { 417 accordion_id = toggle_href; 418 return false; 419 } 420 } ); 421 422 return accordion_id; 423 }; 424 425 /** 426 * Checks if this map is inside a hidden Bootstrap tab. 427 * 428 * @param {String} map_selector 429 * 430 * @return {string} 431 * 432 * @since 1.4.2 433 */ 434 self.check_being_inside_hidden_bootstrap_tab = function( map_selector ) { 435 var tab_toggle = ''; 436 var toggle_href = ''; 437 var $tab_body; 438 439 $('a[data-toggle="tab"]').each( function( index, toggle ) { 440 toggle_href = $( toggle ).attr('href'); 441 $tab_body = $( toggle_href ); 442 443 if ( !$tab_body.hasClass('active') && $tab_body.has( map_selector ).length ) { 444 tab_toggle = toggle; 445 return false; 446 } 447 } ); 448 449 return tab_toggle; 450 }; 451 452 /** 453 * If not inside hidden Bootstrap accordion or tab, init map, if inside, hook to shown event to init then. 454 * 455 * @param {Object} map 456 * 457 * @since 1.4.2 458 */ 459 self.initMapOrWaitIfInsideHiddenBootstrapAccordionOrTab = function(map ) { 460 var map_selector = '#js-wpv-addon-maps-render-' + map.map; 461 var accordion_id = self.check_being_inside_collapsed_bootstrap_accordion( map_selector ); 462 var tab_toggle = self.check_being_inside_hidden_bootstrap_tab( map_selector ); 463 464 if ( accordion_id ) { 465 $( accordion_id ).one('shown.bs.collapse', function() { 466 self.initMapByAPI( map ); 467 } ); 468 } else if( tab_toggle ) { 469 $( tab_toggle ).one('shown.bs.tab', function() { 470 self.initMapByAPI( map ); 471 } ); 472 } else { 473 self.initMapByAPI( map ); 474 } 475 }; 476 477 /** 478 * Dispatches the correct map-initializer based on the current API setting (Google/Azure/OSM). 479 */ 480 self.initMapByAPI = function( map ) { 481 if ( API_GOOGLE === window.toolset_maps_config.provider ) { 482 self.init_map( map ); 483 } else if ( API_AZURE === window.toolset_maps_config.provider ) { 484 self.initMapAzure( map ); 485 } else if ( API_OSM === window.toolset_maps_config.provider ) { 486 self.initMapOsm( map ); 487 } 488 }; 489 490 /** 491 * init_maps 492 * 493 * @since 1.0 494 */ 495 496 self.init_maps = function() { 497 if ( !self.maps_data.length ) { 498 // No maps to initialize 499 return; 500 } 501 self.maps_data.map( function( map ) { 502 // If there is a marker on the map with option for map rendering to wait until it's ready, do nothing. 503 // (Marker itself will trigger map rendering when ready.) 504 if ( 505 map.options.render 506 && map.options.render === 'wait' 507 ) { 508 return true; 509 } 510 511 // Handle other maps 512 self.init_map_after_loading_styles( map ); 513 }); 514 }; 515 516 /** 517 * init_map 518 * 519 * @since 1.0 520 */ 521 522 self.init_map = function( map ) { 523 var map_icon = '', 524 map_icon_hover = '', 525 map_settings = { 526 zoom: map.options['general_zoom'] 527 }, 528 event_settings = { 529 map_id: map.map, 530 map_options: map.options 531 }; 532 var spiderfy = false;
533 var clickEvent = 'click'; 534 535 $( document ).trigger( 'js_event_wpv_addon_maps_init_map_started', [ event_settings ] ); 536 537 if ( 538 map.options['general_center_lat'] != '' 539 && map.options['general_center_lon'] != '' 540 ) { 541 map_settings['center'] = { 542 lat: map.options['general_center_lat'], 543 lng: map.options['general_center_lon'] 544 }; 545 } else { 546 map_settings['center'] = { 547 lat: 0, 548 lng: 0 549 }; 550 } 551 552 if ( map.options['draggable'] == 'off' ) { 553 map_settings['draggable'] = false; 554 } 555 556 if ( map.options['scrollwheel'] == 'off' ) { 557 map_settings['scrollwheel'] = false; 558 } 559 560 if ( map.options['double_click_zoom'] == 'off' ) { 561 map_settings['disableDoubleClickZoom'] = true; 562 } 563 564 if ( map.options['map_type_control'] == 'off' ) { 565 map_settings['mapTypeControl'] = false; 566 } 567 568 if ( map.options['full_screen_control'] == 'off' ) { 569 map_settings['fullscreenControl'] = false; 570 } 571 572 if ( map.options['zoom_control'] == 'off' ) { 573 map_settings['zoomControl'] = false; 574 // Newer Google Maps JS renders a combined camera control (zoom + tilt) 575 // that ignores zoomControl:false, so disable it too. See toolsetmaps-608. 576 map_settings['cameraControl'] = false; 577 } 578 579 if ( map.options['street_view_control'] == 'off' ) { 580 map_settings['streetViewControl'] = false; 581 } 582 583 if ( map.options['background_color'] != '' ) { 584 map_settings['backgroundColor'] = map.options['background_color']; 585 } 586 587 if ( map.options['styles'] ) { 588 map_settings['styles'] = map.options['styles']; 589 } 590 591 if ( map.options['map_type'] ) { 592 map_settings['mapTypeId'] = map.options['map_type']; 593 } 594 595 const mapElement = document.getElementById( 'js-wpv-addon-maps-render-' + map.map ); 596 597 // In Gutenberg editor, it might happen that the map gets refreshed a couple of times and the div is missing at 598 // this moment. Just give up right now, it will get rendered later on. 599 if ( ! mapElement ) return; 600 601 self.maps[ map.map ] = new self.api.Map( mapElement, map_settings ); 602 603 $( document ).trigger( 'js_event_wpv_addon_maps_init_map_inited', [ event_settings ] ); 604 605 self.bounds[ map.map ] = new self.api.LatLngBounds(); 606 607 if ( map.options['marker_icon'] != '' ) { 608 map_icon = map.options['marker_icon']; 609 } 610 611 if ( map.options['marker_icon_hover'] != '' ) { 612 map_icon_hover = map.options['marker_icon_hover']; 613 } 614 615 if ( 'on' === map.options['spiderfy'] ) { 616 var oms = new OverlappingMarkerSpiderfier( self.maps[ map.map ], { 617 markersWontMove: true, 618 markersWontHide: true, 619 basicFormatEvents: true 620 }); 621 spiderfy = true; 622 } 623 624 var interaction_icons = {}, 625 interaction_popups = {}; 626 627 map.markers.map( function( marker ) { 628 var marker_lat_long = new self.api.LatLng( marker.markerlat, marker.markerlon ), 629 marker_map_icon = ( marker.markericon == '' ) ? map_icon : marker.markericon, 630 marker_map_icon_hover = ( marker.markericonhover == '' ) ? map_icon_hover : marker.markericonhover, 631 marker_settings = { 632 position: marker_lat_long, 633 optimized: false 634 }; 635 636 // 'default' is just used so that default marker icon doesn't get replaced with global marker icon. Now set 637 // it empty, so the rest of code knows how to handle it. 638 if ( marker_map_icon === 'default' ) { 639 marker_map_icon = ''; 640 } 641 642 if ( spiderfy ) { 643 clickEvent = 'spider_click'; 644 } else { 645 marker_settings.map = self.maps[ map.map ] 646 clickEvent = 'click'; 647 } 648 649 if ( marker_map_icon != '' ) { 650 marker_settings['icon'] = self.makeGoogleIcon( marker_map_icon ); 651 } 652 if ( marker.title != '' ) { 653 marker_settings['title'] = marker.title; 654 } 655 656 self.markers[ map.map ] = self.markers[ map.map ] || {}; 657 658 self.markers[ map.map ][ marker.marker ] = new self.api.Marker(marker_settings); 659 660 self.bounds[ map.map ].extend( self.markers[ map.map ][ marker.marker ].position ); 661 662 if ( 663 'on' === map.options['marker_icon_use_hover'] 664 && ( 665 marker_map_icon != '' 666 || marker_map_icon_hover != '' 667 ) 668 ) { 669 marker_map_icon = ( marker_map_icon == '' ) ? window.toolset_maps_config.markerDefaultUrl : marker_map_icon; 670 marker_map_icon_hover = ( marker_map_icon_hover == '' ) ? marker_map_icon : marker_map_icon_hover; 671 672 if ( marker_map_icon != marker_map_icon_hover ) { 673 self.bindGoogleMarkerHoverIcons( 674 self.markers[ map.map ][ marker.marker ], 675 marker.marker, 676 self.makeGoogleIcon( marker_map_icon ), 677 self.makeGoogleIcon( marker_map_icon_hover ), 678 interaction_icons 679 ); 680 } 681 } 682 683 self.api.event.addListener( self.markers[ map.map ][ marker.marker ], clickEvent, function() { 684 var interactions = self.markerInteractions[ map.map ]; 685 686 if ( interactions ) { 687 interactions.openPopup( marker.marker ); 688 } 689 }); 690 691 if ( marker.markerinfowindow !== '' ) { 692 interaction_popups[ marker.marker ] = marker.markerinfowindow; 693 } 694 695 if ( spiderfy ) { 696 oms.addMarker( self.markers[ map.map ][ marker.marker ] ) 697 } 698 }); 699 700 self.registerMarkerInteractions( 701 map.map, 702 self.getGoogleMarkerInteractions( map.map, interaction_icons, interaction_popups ) 703 ); 704 705 if ( _.size( map.markers ) == 1 ) { 706 if ( map.options['single_zoom'] != '' ) { 707 self.maps[ map.map ].setZoom( map.options['single_zoom'] ); 708 if ( map.options['fitbounds'] == 'on' ) { 709 self.api.event.addListenerOnce( self.maps[ map.map ], 'bounds_changed', function( event ) { 710 self.maps[ map.map ].setZoom( map.options['single_zoom'] ); 711 }); 712 } 713 } 714 if ( map.options['single_center'] == 'on' ) { 715 for ( var mark in self.markers[ map.map ] ) { 716 self.maps[ map.map ].setCenter( self.markers[ map.map ][ mark ].getPosition() ); 717 break; 718 } 719 } 720 } else if ( _.size( map.markers ) > 1 ) { 721 if ( map.options['fitbounds'] == 'on' ) { 722 self.maps[ map.map ].fitBounds( self.bounds[ map.map ] ); 723 } 724 } 725 726 if ( _.contains( self.resize_queue, map.map ) ) { 727 self.keep_map_center_and_resize( self.maps[ map.map ] ); 728 _.reject( self.resize_queue, function( item ) { 729 return item == map.map; 730 }); 731 } 732 733 // Init Street View overlay if lat and long are provided (coming from address) 734 if ( map.options.lat && map.options.long ) { 735 self.initStreetView( event_settings, new google.maps.LatLng( map.options.lat, map.options.long ) ); 736 } 737 738 $( document ).trigger( 'js_event_wpv_addon_maps_init_map_completed', [ event_settings ] ); 739 }; 740 741 self.getOrCreateInfowindow = function( map_id ) { 742 self.infowindows[ map_id ] = self.infowindows[ map_id ] || new self.api.InfoWindow({ content: '' }); 743 744 return self.infowindows[ map_id ]; 745 }; 746 747 self.makeGoogleIcon = function( url ) { 748 return { 749 url: url, 750 scaledSize: url.slice( -4 ).toLowerCase() === '.svg' 751 ? new google.maps.Size( 32, 32 ) 752 : null, 753 }; 754 }; 755 756 self.bindGoogleMarkerHoverIcons = function( marker, marker_id, normal_icon, hover_icon, interaction_icons ) { 757 marker.addListener( 'mouseover', function() { 758 marker.setIcon( hover_icon ); 759 }); 760 marker.addListener( 'mouseout', function() { 761 marker.setIcon( normal_icon ); 762 }); 763 interaction_icons[ marker_id ] = { 764 normal: normal_icon, 765 hover: hover_icon 766 }; 767 }; 768 769 self.getGoogleMarkerInteractions = function( map_id, icons, popups ) { 770 return { 771 icons: icons, 772 popups: popups, 773 openPopup: function( marker_id ) { 774 if ( 775 ! _.has( popups, marker_id ) 776 || ! self.get_map_marker( marker_id, map_id ) 777 ) { 778 return; 779 } 780 781 self.getOrCreateInfowindow( map_id ).setContent( popups[ marker_id ] ); 782 self.openInfowindowWhenMarkerVisible( map_id, marker_id ); 783 }, 784 setHover: function( marker_id, is_hovered ) { 785 var marker = self.get_map_marker( marker_id, map_id ); 786 787 if ( 788 ! marker 789 || ! _.has( icons, marker_id ) 790 ) { 791 return; 792 } 793 794 marker.setIcon( is_hovered ? icons[ marker_id ].hover : icons[ marker_id ].normal ); 795 } 796 }; 797 }; 798 799 /** 800 * Make sure that marker is visible (not in cluster) before opening info
800window. If it isn't, wait until it is. 801 * 802 * Because, if marker isn't visible, info window will get wrong location to open (usually another, visible marker). 803 * 804 * @param {String} mapId 805 * @param {String} markerId 806 * @since 1.5 807 */ 808 self.openInfowindowWhenMarkerVisible = function( mapId, markerId ) { 809 var marker = self.get_map_marker( markerId, mapId ); 810 811 if ( ! marker || ! self.infowindows[ mapId ] ) { 812 return; 813 } 814 815 if ( marker.map ) { 816 self.infowindows[ mapId ].open( self.maps[ mapId ], marker ); 817 } else { 818 _.delay(function () { 819 self.openInfowindowWhenMarkerVisible( mapId, markerId ); 820 }, 150); 821 } 822 }; 823 /** 824 * clean_map_data 825 * 826 * @param map_id 827 * 828 * @since 1.0 829 */ 830 831 self.clean_map_data = function( map_id ) { 832 // Remove Leaflet map instance if it exists 833 if ( API_OSM === window.toolset_maps_config.provider && self.maps[map_id] instanceof L.Map) { 834 // Remove marker group if it exists 835 if (self.markerGroups[map_id]) { 836 self.maps[map_id].removeLayer(self.markerGroups[map_id]); 837 } 838 self.maps[map_id].remove(); 839 // Remove the reference from the DOM element 840 const mapElement = document.getElementById('js-wpv-addon-maps-render-' + map_id); 841 if (mapElement) { 842 delete mapElement._leaflet_map; 843 } 844 } 845 846 self.maps_data = _.filter( self.maps_data, function( map_data_unique ) { 847 return map_data_unique.map != map_id; 848 }); 849 850 self.maps = _.omit( self.maps, map_id ); 851 self.markers = _.omit( self.markers, map_id ); 852 self.markerGroups = _.omit( self.markerGroups, map_id ); 853 self.infowindows = _.omit( self.infowindows, map_id ); 854 self.markerInteractions = _.omit( self.markerInteractions, map_id ); 855 self.azurePinContexts = _.omit( self.azurePinContexts, map_id ); 856 self.bounds = _.omit( self.bounds, map_id ); 857 self.cluster_options = _.omit( self.cluster_options, map_id ); 858 859 var settings = { 860 map_id: map_id 861 }; 862 863 $( document ).trigger( 'js_event_wpv_addon_maps_clean_map_completed', [ settings ] ); 864 865 }; 866 867 /** 868 * keep_map_center_and_resize 869 * 870 * @param map 871 * 872 * @since 1.1 873 */ 874 875 self.keep_map_center_and_resize = function( map ) { 876 var map_iter_center = map.getCenter(); 877 self.api.event.trigger( map, "resize" ); 878 map.setCenter( map_iter_center ); 879 }; 880 881 /** 882 * Init all maps - Azure API version 883 * @since 1.5 884 */ 885 self.initMapsAzure = function() { 886 _.each( self.maps_data, self.resolveGeolocatedMarkerThenInitMapAzure ); 887 }; 888 889 /** 890 * Resolve geolocated marker (if any) 891 * 892 * Among markers for a map, there may be a special one which should trigger current visitor geolocation. (It makes 893 * no sense to have more than one, because the visitor is not a quark). This method checks if there is one such 894 * marker, if geolocation is available, and if the map rendering should wait for geolocation or if that marker 895 * should be added after map and other markers render. 896 * 897 * @since 1.5 898 * 899 * @param {Object} map 900 */ 901 self.resolveGeolocatedMarkerThenInitMapAzure = function( map ) { 902 let geoMarker = _.findWhere( map.markers, {markerlat: 'geo'} ); 903 let renderedMap = null; 904 905 if ( geoMarker ) { 906 let geoMarkerIndex = _.indexOf( map.markers, geoMarker ); 907 908 if ( "immediate" === geoMarker.markerlon ) { 909 // Render map immediately, marker will be added when and if available, center and bounds adjusted if 910 // needed. 911 renderedMap = self.initMapAzure( map ); 912 913 if (navigator.geolocation) { 914 navigator.geolocation.getCurrentPosition(function (position) { 915 map.markers[geoMarkerIndex].markerlat = position.coords.latitude; 916 map.markers[geoMarkerIndex].markerlon = position.coords.longitude; 917 918 if ( 919 map.markers.length === 1 920 && map.options.single_center === "on" 921 ) { 922 let mapCenter = [position.coords.longitude, position.coords.latitude]; 923 renderedMap.setCamera( {center: mapCenter} ); 924 } 925 926 self.maybeFitboundsAzure( map, renderedMap ); 927 928 renderedMap.addEventListener('load', function() { 929 let movePin = self.azurePinContexts[ map.map ]; 930 931 if ( movePin ) { 932 movePin( 933 geoMarker.marker, 934 [position.coords.longitude, position.coords.latitude] 935 ); 936 } 937 } ); 938 }); 939 } 940 } else { 941 if (navigator.geolocation) { 942 navigator.geolocation.getCurrentPosition(function (position) { 943 map.markers[geoMarkerIndex].markerlat = position.coords.latitude; 944 map.markers[geoMarkerIndex].markerlon = position.coords.longitude; 945 946 // We now have position for geo marker, render as usual. 947 renderedMap = self.initMapAzure( map ); 948 }); 949 } else { 950 // No geolocation? Render what we have, without this marker. 951 renderedMap = self.initMapAzure( map ); 952 } 953 } 954 } else { 955 // No geomarker, just render as usual. 956 renderedMap = self.initMapAzure( map ); 957 } 958 959 return renderedMap; 960 }; 961 962 /** 963 * If there is more than 1 marker and fitbounds is requested for this map, find bound points and set camera bounds. 964 * 965 * @since 1.5 966 * 967 * @param {Object} map 968 * @param {atlas.Map} renderedMap 969 */ 970 self.maybeFitboundsAzure = function( map, renderedMap ) { 971 if ( map.markers.length <= 1 || map.options.fitbounds !== "on" ) { 972 // Not fitting bounds. Either markers <=1 or fitbounds is off 973 return; 974 } 975 976 // Collect positions from markers 977 let positions = map.markers.map( function( m ) { 978 let lon = parseFloat( m.markerlon ); 979 let lat = parseFloat( m.markerlat ); 980 return [ lon, lat ]; 981 }); 982 983 // Filter out invalid positions 984 positions = positions.filter( function( pos ) { 985 return !isNaN( pos[0] ) && !isNaN( pos[1] ); 986 }); 987 988 if ( positions.length === 0 ) {
989 // No valid marker positions found 990 return; 991 } 992 993 // Create a bounding box from positions 994 let lons = positions.map( pos => pos[0] ); 995 let lats = positions.map( pos => pos[1] ); 996 let westmost = Math.min( ...lons ); 997 let eastmost = Math.max( ...lons ); 998 let southernmost = Math.min( ...lats ); 999 let northernmost = Math.max( ...lats ); 1000 1001 // Adjust the map's camera to fit the bounds 1002 renderedMap.setCameraBounds({ 1003 bounds: [ westmost, southernmost, eastmost, northernmost ], 1004 padding: 50 1005 }); 1006 }; 1007 1008 /** 1009 * Map type names are sometimes, but not always the same between Google and Azure. 1010 * 1011 * @since 1.5.3 1012 * @param {string} mapType 1013 * @return {string} 1014 */ 1015 self.translateMapTypesToAzure = function( mapType ) { 1016 const translationTable = { 1017 'roadmap': 'road', 1018 'hybrid': 'satellite_road_labels', 1019 'terrain': 'road' // 'terrain' type is not supported by Azure, so render the closest - 'road' 1020 }; 1021 1022 if ( mapType in translationTable ) return translationTable[mapType]; 1023 // 'satellite' type has the same name, so just falls through. 1024 else return mapType; 1025 }; 1026 1027 /** 1028 * Returns value for marker icon set from marker, map, or default 1029 * 1030 * @since 1.5.3 1031 * @param {Object} marker 1032 * @param {Object} mapOptions 1033 * @return {string} 1034 */ 1035 self.getMarkerIconValue = function( marker, mapOptions ) { 1036 if ( marker.markericon ) { 1037 return marker.markericon; 1038 } else if ( mapOptions.marker_icon ) { 1039 return mapOptions.marker_icon; 1040 } else { 1041 return 'pin-red'; 1042 } 1043 }; 1044 1045 /** 1046 * Adjusts the map to ensure the popup is fully visible. 1047 * 1048 * @param {Object} renderedMap The map object that is currently rendered 1049 * @param {Array} coordinates The coordinates of the marker 1050 */ 1051 self.adjustMapToFitPopup = function(renderedMap, coordinates) { 1052 const camera = renderedMap.getCamera(); 1053 const bounds = camera.bounds; 1054 1055 const west = bounds[0]; 1056 const south = bounds[1]; 1057 const east = bounds[2]; 1058 const north = bounds[3]; 1059 1060 const markerLon = coordinates[0]; 1061 const markerLat = coordinates[1]; 1062 1063 // Add padding to ensure the popup is fully visible 1064 const markerPadding = 0.025; 1065 1066 let isAdjusted = false; 1067 1068 // Check if the marker is outside the current bounds 1069 if (markerLon < west + markerPadding || markerLon > east - markerPadding || markerLat < south + markerPadding || markerLat > north - markerPadding) { 1070 isAdjusted = true; 1071 } 1072 1073 // Center the map on the marker 1074 if ( isAdjusted ) { 1075 renderedMap.setCamera({ center: [markerLon, markerLat] }); 1076 } 1077 }; 1078 1079 /** 1080 * Init a map with Azure API 1081 * 1082 * @since 1.5 1083 * 1084 * @param {Object} map 1085 * 1086 * @return {atlas.Map} 1087 */ 1088 self.initMapAzure = function( map ) { 1089 let event_settings = { 1090 map_id: map.map, 1091 map_options: map.options 1092 }; 1093 $( document ).trigger( 'js_event_wpv_addon_maps_init_map_started', [ event_settings ] ); 1094 1095 // Initialize defaults from map options 1096 let defaultCenterLon = parseFloat( map.options.general_center_lon ); 1097 let defaultCenterLat = parseFloat( map.options.general_center_lat ); 1098 let mapCenter = [ 1099 !isNaN(defaultCenterLon) ? defaultCenterLon : 0, 1100 !isNaN(defaultCenterLat) ? defaultCenterLat : 0 1101 ]; 1102 let mapZoom = parseInt( map.options.general_zoom ) || 10; 1103 let cluster = ( map.options.cluster === 'on' ); 1104 1105 // Handle single marker logic 1106 if ( map.markers.length === 1 ) { 1107 if ( 1108 _.isNumber( parseFloat( map.markers[0].markerlon ) ) 1109 && map.options.single_center === "on" 1110 ) { 1111 mapCenter = [ 1112 parseFloat( map.markers[0].markerlon ), 1113 parseFloat( map.markers[0].markerlat ) 1114 ]; 1115 } 1116 1117 mapZoom = parseInt( map.options.single_zoom ) || mapZoom; 1118 } 1119 1120 // Remove possible map loading content. Google Maps does it by itself, Azure doesn't 1121 $( '#js-wpv-addon-maps-render-' + map.map ).empty(); 1122 1123 // Render map 1124 let renderedMap = new atlas.Map( "js-wpv-addon-maps-render-" + map.map, { 1125 "subscription-key": window.toolset_maps_config.azureBrowserKey, 1126 center: mapCenter, 1127 zoom: mapZoom, 1128 scrollZoomInteraction: ( map.options.scrollwheel === 'on' ), 1129 dblClickZoomInteraction: ( map.options.double_click_zoom === 'on' ), 1130 dragPanInteraction: ( map.options.draggable === 'on' ), 1131 style: self.translateMapTypesToAzure( map.options.map_type ) // Ignored with API 1.1, but works with 1.2 1132 } ); 1133 1134 $( document ).trigger( 'js_event_wpv_addon_maps_init_map_inited', [ event_settings ] ); 1135 1136 // Handle multiple markers 1137 if ( map.markers.length > 1 ) { 1138 if ( map.options.fitbounds === 'on' ) { 1139 // Sets fit bounds 1140 self.maybeFitboundsAzure( map, renderedMap ); 1141 } else if ( map.options.multiple_zoom ) {
1142 // Filter out markers with invalid coordinates 1143 let validMarkers = map.markers.filter( function( m ) { 1144 return !isNaN( parseFloat( m.markerlat ) ) && !isNaN( parseFloat( m.markerlon ) ); 1145 }); 1146 if ( validMarkers.length > 0 ) { 1147 // Initialize sums 1148 let sumLat = 0; 1149 let sumLon = 0; 1150 1151 // Sum up all latitudes and longitudes 1152 validMarkers.forEach( function( m ) { 1153 sumLat += parseFloat( m.markerlat ); 1154 sumLon += parseFloat( m.markerlon ); 1155 }); 1156 1157 // Calculate averages 1158 let avgLat = sumLat / validMarkers.length; 1159 let avgLon = sumLon / validMarkers.length; 1160 1161 // Set the map's camera to the average position 1162 renderedMap.setCamera( { 1163 center: [ avgLon, avgLat ], 1164 zoom: parseInt( map.options.multiple_zoom ) 1165 } ); 1166 } 1167 } else { 1168 // Render default map center 1169 renderedMap.setCamera( { 1170 center: [ 0, 20 ], 1171 zoom: 1.5 1172 } ); 1173 } 1174 } 1175 1176 // Create pins (markers) 1177 let pins = _.map( map.markers, function( marker ) { 1178 return new atlas.data.Feature( 1179 new atlas.data.Point( [marker.markerlon, marker.markerlat] ), 1180 { 1181 title: '', 1182 titletext: marker.title, 1183 popup: marker.markerinfowindow, 1184 icon: self.getMarkerIconValue( marker, map.options ), 1185 iconhover: marker.markericonhover ? marker.markericonhover : map.options.marker_icon_hover, 1186 id: marker.marker 1187 }, 1188 marker.marker 1189 ); 1190 } ); 1191 1192 // Create custom icons for pins (if any) 1193 let icons = _.filter( 1194 _.union( 1195 _.pluck( map.markers, 'markericon' ), 1196 _.pluck( map.markers, 'markericonhover' ), 1197 [map.options.marker_icon, map.options.marker_icon_hover] 1198 ) 1199 ); 1200 1201 // Add markers and their events to map (waiting for load event first, otherwise the library throws a style 1202 // rendering warning) 1203 renderedMap.addEventListener('load', function() { 1204 if ( self.maps[ map.map ] && self.maps[ map.map ] !== renderedMap ) { 1205 return; 1206 } 1207 1208 let registeredIcons = []; 1209 1210 _.each( icons, function( icon ) { 1211 var iconImg = document.getElementById( icon ); 1212 1213 if ( iconImg ) { 1214 renderedMap.addIcon( icon, iconImg ); 1215 registeredIcons.push( icon ); 1216 } 1217 } ); 1218 1219 // A pin's own icon property always wins over the layer option, so a pin 1220 // asking for an icon that was never registered draws nothing at all and 1221 // reports no error. Fall back to a built-in sprite icon instead, so the 1222 // marker stays on the map even when its image is missing. 1223 // 1224 // The hover icon needs the same guard: the hover handler assigns 1225 // iconhover onto icon, so an unregistered hover image would make the 1226 // marker vanish on mouseover. 1227 _.each( pins, function( pin ) { 1228 if ( 1229 pin.properties.icon 1230 && ! _.contains( registeredIcons, pin.properties.icon ) 1231 ) { 1232 pin.properties.icon = 'pin-red'; 1233 } 1234 1235 if ( 1236 pin.properties.iconhover 1237 && ! _.contains( registeredIcons, pin.properties.iconhover ) 1238 ) { 1239 pin.properties.iconhover = pin.properties.icon; 1240 } 1241 } ); 1242 1243 self.addPins( renderedMap, pins, cluster ); 1244 1245 // FUTURE: this needs Azure Maps API 1.2, which is buggy at the moment and kills some other things. Revisit. 1246 // Add controls 1247 /*if ( map.options.zoom_control === 'on' ) { 1248 renderedMap.addControl(new atlas.control.ZoomControl, {}); 1249 } 1250 if ( map.options.map_type_control === 'on' ) { 1251 renderedMap.addControl(new atlas.control.StyleControl, {}); 1252 }*/ 1253 1254 // Store the last opened popup 1255 let currentPopup = null; 1256 1257 let closePinTooltip = function() { 1258 renderedMap.getCanvasContainer().removeAttribute( 'title' ); 1259 }; 1260 1261 let openPinTooltip = function( pin ) { 1262 if ( ! pin.properties.titletext ) { 1263 closePinTooltip(); 1264 return; 1265 } 1266 1267 renderedMap.getCanvasContainer().setAttribute( 'title', pin.properties.titletext ); 1268 }; 1269 1270 let originalIcons = {}; 1271 1272 let pinsById = {}; 1273 _.each( pins, function( pin ) { 1274 pinsById[ String( pin.properties.id ) ] = pin; 1275 } ); 1276 1277 let findPin = function( markerId ) { 1278 return pinsById[ String( markerId ) ]; 1279 }; 1280 1281 self.azurePinContexts[ map.map ] = function( markerId, coordinates ) { 1282 let pin = findPin( markerId ); 1283 1284 if ( ! pin ) return; 1285
1286 pin.geometry.coordinates = coordinates; 1287 self.addPins( renderedMap, pins, cluster ); 1288 }; 1289 1290 let openPopupForPin = function( pin ) { 1291 if ( 1292 ! pin 1293 || ! pin.properties.popup 1294 || ! _.every( pin.geometry.coordinates, _.isFinite ) 1295 ) return; 1296 1297 // Close the currently open popup 1298 if ( currentPopup ) { 1299 currentPopup.close(); 1300 } 1301 1302 closePinTooltip(); 1303 1304 // Create content for popup 1305 let popupContentElement = document.createElement("div"); 1306 popupContentElement.className = "wpv-addon-maps-popup-content"; 1307 popupContentElement.style.padding = "8px"; 1308 let popupNameElement = document.createElement("div"); 1309 popupNameElement.innerHTML = pin.properties.popup; 1310 popupContentElement.appendChild(popupNameElement); 1311 1312 // Create a popup 1313 let popup = new atlas.Popup( { 1314 content: popupContentElement, 1315 position: pin.geometry.coordinates, 1316 pixelOffset: [0, 0] 1317 } ); 1318 1319 popup.open( renderedMap ); 1320 currentPopup = popup; 1321 1322 // Ensure the map pans to the popup 1323 self.adjustMapToFitPopup( renderedMap, pin.geometry.coordinates ); 1324 }; 1325 1326 let openPinPopup = function( markerId ) { 1327 openPopupForPin( findPin( markerId ) ); 1328 }; 1329 1330 let restorePinIcon = function( markerId ) { 1331 let original = originalIcons[ markerId ]; 1332 1333 if ( original === undefined ) return false; 1334 1335 delete originalIcons[ markerId ]; 1336 1337 let pin = findPin( markerId ); 1338 1339 if ( ! pin ) return false; 1340 1341 pin.properties.icon = original; 1342 1343 return true; 1344 }; 1345 1346 let setPinHover = function( markerId, isHovered ) { 1347 let pin = findPin( markerId ); 1348 1349 if ( ! pin ) return; 1350 1351 let changed = false; 1352 1353 if ( isHovered ) { 1354 openPinTooltip( pin ); 1355 1356 if ( 1357 map.options.marker_icon_use_hover === 'on' 1358 && originalIcons[ markerId ] === undefined 1359 && pin.properties.iconhover 1360 && pin.properties.iconhover !== pin.properties.icon 1361 ) { 1362 originalIcons[ markerId ] = pin.properties.icon; 1363 pin.properties.icon = pin.properties.iconhover; 1364 changed = true; 1365 } 1366 } else { 1367 closePinTooltip(); 1368 1369 if ( restorePinIcon( markerId ) ) { 1370 changed = true; 1371 } 1372 } 1373 1374 if ( ! changed ) return; 1375 1376 self.addPins( renderedMap, pins, cluster ); 1377 }; 1378 1379 renderedMap.addEventListener( 'click', function() { 1380 if ( currentPopup ) { 1381 currentPopup.close(); 1382 } 1383 }); 1384 1385 renderedMap.addEventListener( 'click', "default-pin-layer", function( event ) { 1386 if ( ! event.features || ! event.features.length ) return; 1387 1388 openPopupForPin( event.features[0] ); 1389 } ); 1390 1391 renderedMap.addEventListener( 'mouseover', "default-pin-layer", function( event ) { 1392 if ( ! event.features || ! event.features.length ) return; 1393 1394 setPinHover( event.features[0].properties.id, true ); 1395 } ); 1396 renderedMap.addEventListener( 'mouseout', "default-pin-layer", function( event ) { 1397 if ( ! event.features || ! event.features.length ) { 1398 closePinTooltip(); 1399 1400 let restored = _.filter( _.keys( originalIcons ), restorePinIcon ); 1401 1402 if ( restored.length ) { 1403 self.addPins( renderedMap, pins, cluster ); 1404 } 1405 return; 1406 } 1407 1408 setPinHover( event.features[0].properties.id, false ); 1409 } ); 1410 1411 self.registerMarkerInteractions( map.map, { 1412 openPopup: openPinPopup, 1413 setHover: setPinHover 1414 } ); 1415 1416 $( document ).trigger( 'js_event_wpv_addon_maps_init_map_completed', [ event_settings ] ); 1417 } ); 1418 1419 self.maps[ map.map ] = renderedMap; 1420 1421 return renderedMap; 1422 }; 1423 1424 /** 1425 * Adds (or changes) all the pins for the given map on default pin layer. 1426 * 1427 * (Because you cannot simply change one pin with Azure, you have to change the layer). 1428 * 1429 * @since 1.5.3 1430 * @param atlas.Map map 1431 * @param {array} pins 1432 * @param {bool} cluster 1433 */ 1434 self.addPins = function( map, pins, cluster ) { 1435 map.addPins( pins, { 1436 fontColor: "#000", 1437 fontSize: 14, 1438 name: "default-pin-layer", 1439 cluster: cluster, 1440 textOffset: [0, 20], 1441 overwrite: true 1442 } ); 1443 }; 1444 1445 // ------------------------------------ 1446 // API 1447 // ------------------------------------ 1448 1449 /** 1450 * WPViews.view_addon_maps.reload_map 1451 * 1452 * @param map_id 1453 * 1454 * @since 1.0 1455 */ 1456 1457 self.reload_map = function( map_id ) { 1458 var settings = { 1459 map_id: map_id 1460 }; 1461 $( document ).trigger( 'js_event_wpv_addon_maps_reload_map_started', [ settings ] ); 1462 $( document ).trigger( 'js_event_wpv_addon_maps_reload_map_triggered', [ settings ] ); 1463 $( document ).trigger( 'js_event_wpv_addon_maps_reload_map_completed', [ settings ] ); 1464 }; 1465 1466 /** 1467 * document.js_event_wpv_addon_maps_reload_map_triggered 1468 * 1469 * @param event 1470 * @param data 1471 * data.map_id 1472 * 1473 * @since 1.0
1474 * @since 1.5 supports Google, Open Street Maps and Azure API 1475 */ 1476 $( document ).on( 'js_event_wpv_addon_maps_reload_map_triggered', function( event, data ) { 1477 var defaults = { 1478 map_id: false 1479 }, 1480 settings = $.extend( {}, defaults, data ); 1481 if ( 1482 settings.map_id == false || 1483 $( '#js-wpv-addon-maps-render-' + settings.map_id ).length != 1 1484 ) { 1485 return; 1486 } 1487 1488 self.clean_map_data( settings.map_id ); 1489 var mpdata = self.collect_map_data( $( '#js-wpv-addon-maps-render-' + settings.map_id ) ); 1490 1491 // Now decide which init to call based on API 1492 if ( API_GOOGLE === window.toolset_maps_config.provider ) { 1493 self.init_map_after_loading_styles( mpdata ); 1494 } else if ( API_AZURE === window.toolset_maps_config.provider ) { 1495 $( '#js-wpv-addon-maps-render-' + settings.map_id ).empty(); 1496 self.resolveGeolocatedMarkerThenInitMapAzure( mpdata ); 1497 } else if ( API_OSM === window.toolset_maps_config.provider ) { 1498 // Re-initialize that single map 1499 $( '#js-wpv-addon-maps-render-' + settings.map_id ).empty(); 1500 self.initMapOsm( mpdata ); 1501 } 1502 }); 1503 1504 /** 1505 * WPViews.view_addon_maps.get_map 1506 * 1507 * @param map_id 1508 * 1509 * @return google.maps.Map object | false 1510 * 1511 * @since 1.0 1512 */ 1513 1514 self.get_map = function( map_id ) { 1515 if ( map_id in self.maps ) { 1516 return self.maps[ map_id ]; 1517 } else { 1518 return false; 1519 } 1520 }; 1521 1522 /** 1523 * WPViews.view_addon_maps.get_map_marker 1524 * 1525 * @param marker_id 1526 * @param map_id 1527 * 1528 * @return google.maps.Marker object | false 1529 * 1530 * @since 1.0 1531 */ 1532 1533 self.get_map_marker = function( marker_id, map_id ) { 1534 if ( 1535 map_id in self.markers 1536 && marker_id in self.markers[ map_id ] 1537 ) { 1538 return self.markers[ map_id ][ marker_id ]; 1539 } else { 1540 return false; 1541 } 1542 }; 1543 1544 // ------------------------------------ 1545 // Interaction 1546 // ------------------------------------ 1547 1548 self.registerMarkerInteractions = function( map_id, adapter ) { 1549 self.markerInteractions[ map_id ] = adapter; 1550 }; 1551 1552 self.getMarkerInteractionTargets = function( element, classname_prefix ) { 1553 var targets = []; 1554 1555 _.each( element.classList, function( classname ) { 1556 _.each( self.markerInteractions, function( adapter, map_id ) { 1557 var prefix = classname_prefix + map_id + '-marker-'; 1558 1559 if ( ! classname.startsWith( prefix ) ) { 1560 return; 1561 } 1562 targets.push( { 1563 map_id: map_id, 1564 marker_id: classname.slice( prefix.length ), 1565 adapter: adapter 1566 } ); 1567 }); 1568 }); 1569 1570 return targets; 1571 }; 1572 1573 self.getMarkerDataElement = function( marker_id, map_id ) { 1574 if ( /\s/.test( String( marker_id ) + String( map_id ) ) ) { 1575 return null; 1576 } 1577 1578 var markerElements = document.getElementsByClassName( 1579 'js-wpv-addon-maps-marker-' + marker_id + ' js-wpv-addon-maps-markerfor-' + map_id 1580 ); 1581 1582 return markerElements.length ? markerElements[ 0 ] : null; 1583 }; 1584 1585 self.getPlainTextTitle = function( value ) { 1586 var text = String( value == null ? '' : value ); 1587 1588 if ( text.indexOf( '<' ) === -1 && text.indexOf( '&' ) === -1 ) { 1589 return text; 1590 } 1591 1592 return new DOMParser().parseFromString( text, 'text/html' ).body.textContent || ''; 1593 }; 1594 1595 self.getMarkerTitle = function( marker_id, map_id ) { 1596 var markerElement = self.getMarkerDataElement( marker_id, map_id ); 1597 1598 return markerElement ? self.getPlainTextTitle( markerElement.getAttribute( 'data-markertitle' ) ) : ''; 1599 }; 1600 1601 self.getMarkerDataLatLng = function( marker_id, map_id ) { 1602 var markerElement = self.getMarkerDataElement( marker_id, map_id ), 1603 lat, 1604 lng; 1605 1606 if ( ! markerElement ) { 1607 return null; 1608 } 1609 1610 lat = parseFloat( markerElement.getAttribute( 'data-markerlat' ) ); 1611 lng = parseFloat( markerElement.getAttribute( 'data-markerlon' ) ); 1612 1613 return isNaN( lat ) || isNaN( lng ) ? null : { lat: lat, lng: lng }; 1614 }; 1615 1616 /** 1617 * Reload on js-wpv-addon-maps-reload-map.click 1618 * 1619 * @since 1.0 1620 */ 1621 1622 $( document ).on( 'click', '.js-wpv-addon-maps-reload-map', function( e ) { 1623 e.preventDefault(); 1624 var thiz = $( this ); 1625 if ( thiz.attr( 'data-map' ) ) { 1626 self.reload_map( thiz.data( 'map' ) ); 1627 } 1628 }); 1629 1630 /** 1631 * Center on a marker on js-wpv-addon-maps-center-map.click 1632 * 1633 * @since 1.0 1634 * @since 1.5 Azure API supported 1635 */ 1636 $( document ).on( 'click', '.js-wpv-addon-maps-focus-map', function( e ) { 1637 e.preventDefault(); 1638 var thiz = $( this ), 1639 thiz_map, 1640 thiz_marker, 1641 thiz_zoom; 1642 if ( 1643 thiz.attr( 'data-map' ) && 1644 thiz.attr( 'data-marker' ) 1645 ) { 1646 thiz_map = thiz.data( 'map' ); 1647 thiz_marker = thiz.data( 'marker' ); 1648 1649 if ( API_GOOGLE === window.toolset_maps_config.provider ) { 1650 if ( 1651 thiz_map in self.maps && 1652 thiz_map in self.markers && 1653 thiz_marker in self.markers[thiz_map] 1654 ) { 1655 thiz_zoom = ($('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom') != '') 1656 ? $('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom') 1657 : 14; 1658 self.maps[thiz_map].setCenter(self.markers[thiz_map][thiz_marker].getPosition()); 1659 self.maps[thiz_map].setZoom(thiz_zoom); 1660 } 1661 } else if ( API_AZURE === window.toolset_maps_config.provider ) { 1662 if ( thiz_map in self.maps ) { 1663 thiz_zoom = ($('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom') != '') 1664 ? $('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom') 1665 : 14; 1666 var thiz_marker_position = self.getMarkerDataLatLng( thiz_marker, thiz_map ); 1667 1668 if ( thiz_marker_position ) { 1669 self.maps[thiz_map].setCamera({ 1670 center: [thiz_marker_position.lng, thiz_marker_position.lat], 1671 zoom: thiz_zoom 1672 }); 1673 } 1674 } 1675 } else if ( API_OSM === window.toolset_maps_config.provider ) { 1676 if ( thiz_map in self.maps ) { 1677 thiz_zoom = parseInt( $('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom' ), 10 ) || 14; 1678 1679 var thiz_marker_object = self.get_map_marker( thiz_marker, thiz_map ), 1680 thiz_marker_position = thiz_marker_object 1681 ? thiz_marker_object.getLatLng() 1682 : self.getMarkerDataLatLng( thiz_marker, thiz_map ); 1683 1684 if ( thiz_marker_position ) {
1685 self.maps[thiz_map].setView([thiz_marker_position.lat, thiz_marker_position.lng], thiz_zoom); 1686 } 1687 } 1688 } 1689 } 1690 }); 1691 1692 $( document ).on( 'click', '[class*="js-toolset-maps-open-infowindow-map-"]', function( e ) { 1693 var targets = self.getMarkerInteractionTargets( this, 'js-toolset-maps-open-infowindow-map-' ); 1694 1695 if ( ! targets.length ) { 1696 return; 1697 } 1698 1699 if ( 1700 'A' === this.tagName 1701 && '#' === this.getAttribute( 'href' ) 1702 ) { 1703 e.preventDefault(); 1704 } 1705 1706 _.each( targets, function( target ) { 1707 target.adapter.openPopup( target.marker_id ); 1708 }); 1709 }); 1710 1711 self.maybeSetMarkerInteractionTitle = function( element, marker_id, map_id ) { 1712 var current = element.getAttribute( 'title' ), 1713 ours = current && current === element.getAttribute( 'data-toolset-maps-title' ), 1714 title; 1715 1716 if ( null !== current && ! ours ) { 1717 return; 1718 } 1719 1720 title = self.getMarkerTitle( marker_id, map_id ); 1721 1722 if ( ( current || '' ) === title ) { 1723 return; 1724 } 1725 1726 if ( ! title ) { 1727 element.removeAttribute( 'title' ); 1728 element.removeAttribute( 'data-toolset-maps-title' ); 1729 return; 1730 } 1731 1732 element.setAttribute( 'title', title ); 1733 element.setAttribute( 'data-toolset-maps-title', title ); 1734 }; 1735 1736 $( document ).on( 'mouseenter mouseleave', '[class*="js-toolset-maps-hover-map-"]', function( e ) { 1737 var element = this, 1738 is_hovered = 'mouseenter' === e.type; 1739 1740 _.each( self.getMarkerInteractionTargets( element, 'js-toolset-maps-hover-map-' ), function( target ) { 1741 if ( is_hovered ) { 1742 self.maybeSetMarkerInteractionTitle( element, target.marker_id, target.map_id ); 1743 } 1744 1745 target.adapter.setHover( target.marker_id, is_hovered ); 1746 }); 1747 }); 1748 1749 /** 1750 * Center map on fitbounds on js-wpv-addon-maps-center-map-fitbounds.click 1751 * 1752 * @since 1.0 1753 */ 1754 $( document ).on( 'click', '.js-wpv-addon-maps-restore-map', function( e ) { 1755 e.preventDefault(); 1756 var thiz = $( this ), 1757 thiz_map, 1758 current_map_data_array, 1759 current_map_data; 1760 if ( thiz.attr( 'data-map' ) ) { 1761 thiz_map = thiz.data( 'map' ); 1762 1763 if ( API_GOOGLE === window.toolset_maps_config.provider ) { 1764 if ( 1765 thiz_map in self.maps 1766 && thiz_map in self.bounds 1767 ) { 1768 self.maps[ thiz_map ].fitBounds( self.bounds[ thiz_map ] ); 1769 } 1770 1771 if ( _.size( self.markers[ thiz_map ] ) == 1 ) { 1772 current_map_data_array = _.filter( self.maps_data, function( map_data_unique ) { 1773 return map_data_unique.map == thiz_map; 1774 }); 1775 current_map_data = _.first( current_map_data_array ); 1776 if ( current_map_data.options['single_zoom'] != '' ) { 1777 self.maps[ thiz_map ].setZoom( current_map_data.options['single_zoom'] ); 1778 if ( current_map_data.options['fitbounds'] == 'on' ) { 1779 self.api.event.addListenerOnce( self.maps[ thiz_map ], 'bounds_changed', function( event ) { 1780 self.maps[ thiz_map ].setZoom( current_map_data.options['single_zoom'] ); 1781 }); 1782 } 1783 } 1784 if ( current_map_data.options['single_center'] == 'on' ) { 1785 for ( var mark in self.markers[ thiz_map ] ) { 1786 self.maps[ thiz_map ].setCenter( self.markers[ thiz_map ][ mark ].getPosition() ); 1787 break; 1788 } 1789 } 1790 } 1791 } 1792 1793 if ( API_AZURE === window.toolset_maps_config.provider ) { 1794 current_map_data_array = _.filter( self.maps_data, function( map_data_unique ) { 1795 return map_data_unique.map == thiz_map; 1796 }); 1797 current_map_data = _.first( current_map_data_array ); 1798 1799 // Get center latitude and longitude 1800 var centerLat = parseFloat( current_map_data.options.general_center_lat ); 1801 var centerLon = parseFloat( current_map_data.options.general_center_lon ); 1802 1803 // Check if centerLat or centerLon is exactly 0 or invalid 1804 if ( centerLat === 0 || centerLon === 0 || isNaN( centerLat ) || isNaN( centerLon ) ) { 1805 1806 // Proceed to adjust the map based on markers - calculating center from markers 1807 if ( _.size( current_map_data.markers ) === 1 ) { 1808 var markerLat = parseFloat( current_map_data.markers[0].markerlat ); 1809 var markerLon = parseFloat( current_map_data.markers[0].markerlon ); 1810 var zoom_level = current_map_data.options.single_zoom ? parseInt( current_map_data.options.single_zoom ) : 15;
1811 if ( !isNaN( markerLat ) && !isNaN( markerLon ) ) { 1812 // Setting map camera to center on single marker 1813 self.maps[ thiz_map ].setCamera( { 1814 center: [ markerLon, markerLat ], 1815 zoom: zoom_level 1816 } ); 1817 } 1818 } else if ( _.size( current_map_data.markers ) > 1 ) { 1819 1820 // Multiple markers detected - filter valid markers 1821 var validMarkers = current_map_data.markers.filter( function( m ) { 1822 return !isNaN( parseFloat( m.markerlat ) ) && !isNaN( parseFloat( m.markerlon ) ); 1823 } ); 1824 1825 if ( validMarkers.length > 0 ) { 1826 // Compute average center 1827 var avgLon = _.meanBy( validMarkers, function( m ) { return parseFloat( m.markerlon ); } ); 1828 var avgLat = _.meanBy( validMarkers, function( m ) { return parseFloat( m.markerlat ); } ); 1829 var multipleZoom = parseInt( current_map_data.options.multiple_zoom ) || 10; 1830 if ( !isNaN( avgLat ) && !isNaN( avgLon ) ) { 1831 // Setting center to average position and zoom to multiple_zoom 1832 self.maps[ thiz_map ].setCamera( { 1833 center: [ avgLon, avgLat ], 1834 zoom: multipleZoom 1835 } ); 1836 } 1837 } 1838 } 1839 } else { 1840 // Using provided center latitude and longitude 1841 if ( _.size( current_map_data.markers ) === 1 ) { 1842 var zoom_level = current_map_data.options.single_zoom ? parseInt( current_map_data.options.single_zoom ) : 15; 1843 if ( !isNaN( centerLat ) && !isNaN( centerLon ) ) { 1844 self.maps[ thiz_map ].setCamera( { 1845 center: [ centerLon, centerLat ], 1846 zoom: zoom_level 1847 } ); 1848 } else { 1849 console.warn( "Invalid center coordinates provided." ); 1850 } 1851 } else if ( _.size( current_map_data.markers ) > 1 ) { 1852 // Use fitbounds if available 1853 if ( current_map_data.options.fitbounds == 'on' ) { 1854 self.maybeFitboundsAzure( current_map_data, self.maps[ thiz_map ] ); 1855 } else { 1856 // Fitbounds not on, center map using provided center 1857 var multipleZoom = parseInt( current_map_data.options.multiple_zoom ) || 10; 1858 if ( !isNaN( centerLat ) && !isNaN( centerLon ) ) { 1859 self.maps[ thiz_map ].setCamera( { 1860 center: [ centerLon, centerLat ], 1861 zoom: multipleZoom 1862 } ); 1863 } else { 1864 console.warn( "Invalid center coordinates provided." ); 1865 } 1866 } 1867 } 1868 } 1869 } 1870 } 1871 }); 1872 1873 // ------------------------------------ 1874 // Views compatibility 1875 // ------------------------------------ 1876 1877 /** 1878 * Reload the maps contained on the parametric search results loaded via AJAX 1879 * 1880 * @since 1.0 1881 */ 1882 $( document ).on( 'js_event_wpv_parametric_search_results_updated', function( event, data ) { 1883 // Check if markers are present in the new layout 1884 if ( !data.layout.find( '.js-wpv-addon-maps-marker' ).length ) { 1885 // Check if data.response and data.response.full exist 1886 if ( data.response && data.response.full ) { 1887 // If not present, find markers in the original response data and append to current layout
1888 var markerDivs = $( data.response.full ).find( '.js-wpv-addon-maps-marker' ); 1889 if ( markerDivs.length ) { 1890 data.layout.append( markerDivs ); 1891 } 1892 } 1893 } 1894 self.cleanupMarkersFromMapBlock( data.view_unique_id ); 1895 _.each( self.get_affected_maps( data.layout ), self.reload_map ); 1896 }); 1897 1898 /** 1899 * Reload the maps contained on the pagination results loaded via AJAX 1900 * 1901 * @since 1.0 1902 */ 1903 $( document ).on( 'js_event_wpv_pagination_completed', function( event, data ) { 1904 self.cleanupMarkersFromMapBlock( data.view_unique_id ); 1905 _.each( self.get_affected_maps( data.layout ), self.reload_map ); 1906 } ); 1907 1908 /** 1909 * Check if a Map block contains markers coming from a View, and it it does, remove those, because new ones are 1910 * provided by View. 1911 * 1912 * @since 2.0 1913 * @param {int|string} viewUniqueId 1914 */ 1915 self.cleanupMarkersFromMapBlock = function( viewUniqueId ) { 1916 const viewId = _.isNumber( viewUniqueId ) ? viewUniqueId : viewUniqueId.split( '-' )[ 0 ]; 1917 1918 $( '.js-wpv-addon-maps-marker' ).each( function( key, marker ) { 1919 // If this is a marker coming from given View... 1920 if ( Number( $( marker ).data( 'fromview' ) ) === Number( viewId ) ) { 1921 // Record View - Map blocks relation, as it's useful in some situations 1922 self.view_map_blocks_relation[ viewUniqueId ] = $( marker ).data( 'markerfor' ); 1923 1924 // Remove this View's markers that sit next to a Map block, so the 1925 // refresh can supply new ones. Match on sharing a parent with the 1926 // map render div rather than on being its immediate next sibling. 1927 // 1928 // This is deliberately wider than the old adjacency test, on every 1929 // provider: only the first marker is ever adjacent to the map div, 1930 // so the old check left the rest behind. Azure made that visible, 1931 // because it prints hidden marker icon images between the map and 1932 // the markers, leaving nothing adjacent at all and piling stale 1933 // markers onto the map with every AJAX refresh. 1934 if ( $( marker ).siblings( '.js-wpv-addon-maps-render' ).length ) { 1935 $( marker ).remove(); 1936 } 1937 } 1938 } ); 1939 }; 1940 1941 /** 1942 * get_affected_maps 1943 * 1944 * Get all the maps that have related data used in the given containr, no matter if a map render or a marker 1945 * 1946 * @param container object 1947 * 1948 * @return array 1949 * 1950 * @since 1.1 1951 */ 1952 1953 self.get_affected_maps = function( container ) { 1954 var affected_maps = []; 1955 container.find( '.js-wpv-addon-maps-render' ).each( function() { 1956 affected_maps.push( $( this ).data( 'map' ) ); 1957 }); 1958 container.find( '.js-wpv-addon-maps-marker' ).each( function() { 1959 affected_maps.push( $( this ).data( 'markerfor' ) ); 1960 }); 1961 if ( self.view_map_blocks_relation[ container.data( 'viewnumber' ) ] ) { 1962 affected_maps.push( self.view_map_blocks_relation[ container.data( 'viewnumber' ) ] ); 1963 } 1964 affected_maps = _.uniq( affected_maps ); 1965 return affected_maps; 1966 }; 1967 1968 // ------------------------------------ 1969 // Clusters definitions and interactions 1970 // ------------------------------------ 1971 1972 /** 1973 * WPViews.view_addon_maps.set_cluster_options 1974 * 1975 * Sets options for clusters, either global or for a specific map 1976 * 1977 * @param options object 1978 * @param options.styles = [ 1979 * { 1980 * url: string URL of the cluster image for this style, 1981 * height: integer Width of the cluster image for this style, 1982 * width: integer Height of the cluster image for this style, 1983 * textColor: string (optional) Color of the counter text in this cluster, as a color name or hex value (with #), 1984 * textSize integer (optional) Text size for the counter in this cluster, in px (without unit) 1985 * }, 1986 * { ... } 1987 * ] 1988 * @param options.calculator = function( markers, numStyles ) { 1989 * @param markers array Markers in this cluster 1990 * @param numStyles integer Number of styles defined 1991 * @return { 1992 * text: string Text to be displayed inside the marker, 1993 * index: integer Index of the options.styles array that will be applied to this cluster - please make it less than numStyles 1994 * }; 1995 * } 1996 * @param map_id (optional) string The map ID this options will be binded to, global otherwise 1997 * 1998 * @note Most of the cluster options for a map are set in the map shortcode 1999 * @note Maps without specific styling options will get the current global options 2000 * @note We stoe the options in self.has_cluster_options for later usage, like on reload events 2001 * 2002 * @since 1.0 2003 */ 2004 2005 self.set_cluster_options = function( options, map_id ) { 2006 if ( typeof map_id === 'undefined' ) { 2007 // If map_id is undefined, set global options 2008 if ( _.has( options , "styles" ) ) { 2009 self.default_cluster_options['styles'] = options['styles']; 2010 } 2011 if ( _.has( options , "calculator" ) ) { 2012 self.default_cluster_options['calculator'] = options['calculator']; 2013 } 2014 } else { 2015 // Otherwise, bind to a specific map ID 2016 // Note that defaults are also used 2017 self.cluster_options[ map_id ] = self.get_cluster_options( map_id ); 2018 if ( _.has( options , "styles" ) ) { 2019 self.cluster_options[ map_id ]['styles'] = options['styles']; 2020 } 2021 if ( _.has( options , "calculator" ) ) { 2022 self.cluster_options[ map_id ]['calculator'] = options['calculator']; 2023 } 2024 self.has_cluster_options[ map_id ] = options; 2025 } 2026 }; 2027 2028 /** 2029 * WPViews.view_addon_maps.get_cluster_options 2030 *
2031 * Gets options for clusters, either global of for a specific map 2032 * 2033 * @param map_id (optional) string The map ID to get options from 2034 * 2035 * @return options object Set of options, either global or dedicated if the passed map_id has specific options 2036 * 2037 * @since 1.0 2038 */ 2039 2040 self.get_cluster_options = function( map_id ) { 2041 var options = self.default_cluster_options; 2042 if ( 2043 typeof map_id !== 'undefined' 2044 && _.has( self.cluster_options, map_id ) 2045 ) { 2046 options = self.cluster_options[ map_id ]; 2047 } 2048 return options; 2049 }; 2050 2051 // ------------------------------------ 2052 // Init 2053 // ------------------------------------ 2054 2055 /** 2056 * Inits Google Maps API specific code paths 2057 * @since 1.5 2058 */ 2059 self.initGoogle = function() { 2060 if ( self.api === null ) { 2061 if ( self.maps_data.length ) { 2062 self.maybeLazyLoadGoogle(); 2063 } 2064 return; 2065 } else if ( self.api === false ) { // Lazy loading failed, don't get into an infinite loop. 2066 return; 2067 } 2068 2069 window.addEventListener( "resize", function() { 2070 _.each(self.maps, function( map_iter, map_id ) { 2071 self.keep_map_center_and_resize( map_iter ); 2072 }); 2073 }); 2074 2075 $( document ).on( 'js_event_wpv_layout_responsive_resize_completed', function( event ) { 2076 $( '.js-wpv-layout-responsive .js-wpv-addon-maps-render' ).each( function() { 2077 var thiz = $( this ), 2078 thiz_map = thiz.data( 'map' ); 2079 if ( thiz_map in self.maps ) { 2080 self.keep_map_center_and_resize( self.maps[ thiz_map ] ); 2081 } else { 2082 self.resize_queue.push( thiz_map ); 2083 _.uniq( self.resize_queue ); 2084 } 2085 }); 2086 }); 2087 2088 self.maybeInitElementorEditorPreviewFix( self.init_maps ); 2089 2090 self.init_maps(); 2091 }; 2092 2093 /** 2094 * Lazily loads the Azure Maps SDK if it's not already loaded and execute a callback upon completion 2095 * 2096 * This function checks if the Azure Maps SDK is currently loading or already loaded 2097 * - If the SDK is loading, it attaches the provided callback to the `azureMapsLoaded` event 2098 * - If the SDK is already loaded, it immediately invokes the callback 2099 * - If the SDK is not loaded, it dynamically appends the necessary CSS and JS files to the document, 2100 * sets the loading state, and triggers the callback once loading is successful 2101 * 2102 * @param {Function} callback - The function to execute after the Azure Maps SDK has been loaded 2103 * 2104 */
2105 self.maybeLazyLoadAzure = function( callback ) { 2106 if ( self.azureLoading ) { 2107 // SDK is already loading, wait for it to load 2108 $( document ).on( 'azureMapsLoaded', callback ); 2109 return; 2110 } 2111 2112 if ( typeof atlas !== 'undefined' && typeof atlas.DataSource !== 'undefined' ) { 2113 // SDK is already loaded 2114 callback(); 2115 return; 2116 } 2117 2118 self.azureLoading = true; 2119 2120 // Load Azure Maps CSS 2121 var link = document.createElement( 'link' ); 2122 link.rel = 'stylesheet'; 2123 link.href = 'https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css'; 2124 document.head.appendChild(link); 2125 2126 // Load Azure Maps JS 2127 $.getScript('https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js') 2128 .done(function() { 2129 self.azureLoading = false; 2130 $(document).trigger('azureMapsLoaded'); 2131 callback(); 2132 }) 2133 .fail(function() { 2134 self.azureLoading = false; 2135 console.warn('Failed to load Azure Maps SDK'); 2136 }); 2137 }; 2138 2139 /** 2140 * Inits Microsoft Azure Maps specific code paths 2141 * @since 1.5 2142 */ 2143 self.initAzure = function() { 2144 if (typeof atlas === 'undefined') { 2145 self.maybeLazyLoadAzure(self.initMapsAzure); 2146 } else { 2147 self.initMapsAzure(); 2148 } 2149 self.maybeInitElementorEditorPreviewFix( self.initMapsAzure ); 2150 }; 2151 2152 /** 2153 * Initializes all OSM/Leaflet maps based on self.maps_data 2154 */ 2155 self.initOsm = function() { 2156 // Check for dependencies 2157 if (typeof L === 'undefined') { 2158 console.warn('Leaflet (L) not found. Make sure Leaflet JS/CSS is enqueued.'); 2159 return; 2160 } 2161 if (typeof _ === 'undefined') { 2162 console.error('Underscore.js is not loaded. Please include it before this script.'); 2163 return; 2164 } 2165 2166 // Loop over all maps 2167 _.each(self.maps_data, function(map) { 2168 self.initMapOsm(map); 2169 }); 2170 }; 2171 2172 self.createOsmMarkerGroup = function (clusterOptions, cluster) { 2173 const clusteringEnabled = clusterOptions && 2174 cluster === 'on' && 2175 typeof L.markerClusterGroup === 'function'; 2176 2177 if (!clusteringEnabled) return L.featureGroup(); 2178 2179 const minClusterSize = parseInt(clusterOptions.minimumClusterSize, 10) || 2; 2180 2181 const clusterGroup = L.markerClusterGroup({ 2182 maxClusterRadius: parseInt(clusterOptions.gridSize, 10) || 60, 2183 disableClusteringAtZoom: clusterOptions.maxZoom ? parseInt(clusterOptions.maxZoom, 10) : undefined, 2184 zoomToBoundsOnClick: clusterOptions.zoomOnClick !== false, 2185 spiderfyOnMaxZoom: false, 2186 showCoverageOnHover: false, 2187 removeOutsideVisibleBounds: true, 2188 iconCreateFunction: function (cl) { 2189 const n = cl.getChildCount(); 2190 // Hide icon for below-min clusters 2191 if (n < minClusterSize) { 2192 return new L.DivIcon({ html: '', className: 'leaflet-marker-cluster-hidden', iconSize: L.point(0, 0) }); 2193 } 2194 2195 // Normal cluster icon 2196 let iconNumber, iconSize; 2197 if (n < 10) { iconNumber = 1; iconSize = 53; } 2198 else if (n < 100) { iconNumber = 2; iconSize = 56; } 2199 else if (n < 1000) { iconNumber = 3; iconSize = 66; } 2200 else if (n < 10000){ iconNumber = 4; iconSize = 78; } 2201 else { iconNumber = 5; iconSize = 90; } 2202 2203 const url = window.toolset_maps_config.clusterImagePath + iconNumber + '.png'; 2204 return new L.DivIcon({ 2205 html: '<div style="position:relative;width:' + iconSize + 'px;height:' + iconSize + 'px;"><img src="' + url + '" class="leaflet-cluster-image"/><span class="leaflet-cluster-span">' + n + '</span></div>', 2206 className: 'marker-cluster-custom', 2207 iconSize: L.point(iconSize, iconSize), 2208 iconAnchor: L.point(iconSize / 2, iconSize / 2) 2209 }); 2210 } 2211 }); 2212 2213 // Singles rendered as plain markers (FeatureGroup so it has getBounds) 2214 const singlesGroup = L.featureGroup(); 2215 2216 // Composite container (so your existing .addLayer/.fitBounds keep working) 2217 const group = L.featureGroup([clusterGroup, singlesGroup]); 2218 2219 group.clusterGroup = clusterGroup; 2220 2221 // Helpers 2222 const isCluster = (layer) => 2223 layer && typeof layer.getChildCount === 'function' && typeof layer.getAllChildMarkers === 'function'; 2224
2225 function promoteSmallClusters() { 2226 if (!clusterGroup._featureGroup) return; 2227 const moveOut = []; 2228 clusterGroup._featureGroup.eachLayer((layer) => { 2229 if (isCluster(layer) && layer.getChildCount() < minClusterSize) { 2230 layer.getAllChildMarkers().forEach(m => moveOut.push(m)); 2231 } 2232 }); 2233 moveOut.forEach(m => { 2234 if (clusterGroup.hasLayer(m)) clusterGroup.removeLayer(m); 2235 if (!singlesGroup.hasLayer(m)) singlesGroup.addLayer(m); 2236 }); 2237 } 2238 2239 function reintegrateSingles() { 2240 const back = []; 2241 singlesGroup.eachLayer(m => back.push(m)); 2242 back.forEach(m => { 2243 singlesGroup.removeLayer(m); 2244 clusterGroup.addLayer(m); 2245 }); 2246 } 2247 2248 group.addLayer = function (layer) { 2249 if (layer instanceof L.Marker || typeof layer.getLatLng === 'function') { 2250 clusterGroup.addLayer(layer); 2251 } else { 2252 singlesGroup.addLayer(layer); 2253 } 2254 return this; 2255 }; 2256 group.removeLayer = function (layer) { 2257 if (singlesGroup.hasLayer(layer)) singlesGroup.removeLayer(layer); 2258 if (clusterGroup.hasLayer(layer)) clusterGroup.removeLayer(layer); 2259 return this; 2260 }; 2261 group.clearLayers = function () { singlesGroup.clearLayers(); clusterGroup.clearLayers(); return this; }; 2262 group.getBounds = function () { 2263 const b = L.latLngBounds(); 2264 if (clusterGroup.getBounds) b.extend(clusterGroup.getBounds()); 2265 if (singlesGroup.getBounds) b.extend(singlesGroup.getBounds()); 2266 return b; 2267 }; 2268 2269 // Run after each recluster 2270 clusterGroup.on('clusteringend', () => { 2271 // Defer to ensure clusters are fully built 2272 requestAnimationFrame(() => promoteSmallClusters()); 2273 }); 2274 2275 // Bind to the actual map automatically (no need for self.map) 2276 let boundMap = null; 2277 function bindMap(map) { 2278 if (boundMap === map) return; 2279 if (boundMap) { 2280 boundMap.off('zoomstart', reintegrateSingles); 2281 boundMap.off('movestart', reintegrateSingles); 2282 boundMap.off('zoomend', promoteSmallClusters); 2283 boundMap.off('moveend', promoteSmallClusters); 2284 } 2285 boundMap = map; 2286 if (map) { 2287 map.on('zoomstart', reintegrateSingles); 2288 map.on('movestart', reintegrateSingles); 2289 map.on('zoomend', promoteSmallClusters); 2290 map.on('moveend', promoteSmallClusters); 2291 // first pass once attached 2292 setTimeout(promoteSmallClusters, 0); 2293 } 2294 } 2295 2296 group.on('add', () => bindMap(group._map || clusterGroup._map || singlesGroup._map || null)); 2297 group.on('remove', () => bindMap(null)); 2298 2299 // no coverage polygons 2300 clusterGroup._originalShouldShowCoverageOnHover = clusterGroup._shouldShowCoverageOnHover; 2301 clusterGroup._shouldShowCoverageOnHover = function () { return false; }; 2302 2303 return group; 2304 }; 2305 2306 self.makeOsmIcon = function( iconUrl ) { 2307 return L.icon( { 2308 iconUrl: iconUrl, 2309 iconSize: [35, 45], 2310 iconAnchor: [20, 40], 2311 popupAnchor: [0, -40] 2312 } ); 2313 }; 2314
2315 self.buildOsmMarker = function( markerData, mapOptions, interactionIcons ) { 2316 let mLat = parseFloat( markerData.markerlat ); 2317 let mLon = parseFloat( markerData.markerlon ); 2318 2319 if ( isNaN( mLat ) || isNaN( mLon ) ) { 2320 return false; 2321 } 2322 2323 let iconUrl = markerData.markericon || mapOptions.marker_icon || ''; 2324 if ( iconUrl === 'default' ) iconUrl = ''; 2325 2326 let markerOptions = {}; 2327 if ( iconUrl ) { 2328 markerOptions.icon = self.makeOsmIcon( iconUrl ); 2329 } 2330 2331 if ( markerData.title ) { 2332 markerOptions.title = markerData.title; 2333 } 2334 2335 let leafletMarker = L.marker( [ mLat, mLon ], markerOptions ); 2336 2337 if ( markerData.markerinfowindow ) { 2338 leafletMarker.bindPopup( markerData.markerinfowindow ); 2339 } 2340 2341 let hoverUrl = mapOptions.marker_icon_use_hover === 'on' 2342 ? ( markerData.markericonhover || mapOptions.marker_icon_hover || '' ) 2343 : ''; 2344 if ( hoverUrl && hoverUrl !== 'default' && hoverUrl !== iconUrl ) { 2345 let hoverIcon = self.makeOsmIcon( hoverUrl ); 2346 let normalIcon = leafletMarker.options.icon || new L.Icon.Default(); 2347 2348 leafletMarker.on( 'mouseover', function() { 2349 leafletMarker.setIcon( hoverIcon ); 2350 } ); 2351 leafletMarker.on( 'mouseout', function() { 2352 leafletMarker.setIcon( normalIcon ); 2353 } ); 2354 2355 interactionIcons[ markerData.marker ] = { 2356 normal: normalIcon, 2357 hover: hoverIcon 2358 }; 2359 } 2360 2361 return leafletMarker; 2362 }; 2363 2364 self.getOsmMarkerInteractions = function( mapId, interactionIcons ) { 2365 return { 2366 openPopup: function( markerId ) { 2367 let marker = self.get_map_marker( markerId, mapId ); 2368 2369 if ( 2370 ! marker 2371 || ! marker.getPopup() 2372 ) return; 2373 2374 let group = self.markerGroups[ mapId ]; 2375 let clusterGroup = ( group && group.clusterGroup ) || false; 2376 2377 if ( 2378 ! marker._map 2379 && clusterGroup 2380 && clusterGroup.hasLayer( marker ) 2381 ) { 2382 clusterGroup.zoomToShowLayer( marker, function() { 2383 marker.openPopup(); 2384 } ); 2385 return; 2386 } 2387 2388 marker.openPopup(); 2389 }, 2390 setHover: function( markerId, isHovered ) { 2391 let marker = self.get_map_marker( markerId, mapId ); 2392 2393 if ( 2394 ! marker 2395 || ! _.has( interactionIcons, markerId ) 2396 ) return; 2397 2398 marker.setIcon( isHovered ? interactionIcons[ markerId ].hover : interactionIcons[ markerId ].normal ); 2399 } 2400 }; 2401 }; 2402 2403 /** 2404 * Renders a single map with Leaflet using OSM tiles 2405 * @param {Object} map - The map object from self.maps_data (map.options, map.markers, etc.) 2406 */ 2407 self.initMapOsm = function(map) { 2408 if (self.maps[map.map]) { 2409 console.warn(`Map with ID ${map.map} is already initialized. Skipping.`); 2410 return; 2411 } 2412 2413 // Check if 'map' and 'map.map' are defined 2414 if (!map || !map.map) { 2415 console.error('initOsm: Invalid map data received:', map); 2416 return; 2417 } 2418 2419 // If there's no such element - skip 2420 const mapElement = document.getElementById('js-wpv-addon-maps-render-' + map.map); 2421 if (!mapElement) { 2422 console.warn(`Map element with ID js-wpv-addon-maps-render-${map.map} not found.`); 2423 return; 2424 } 2425 2426 // Define default center 2427 let lat = parseFloat(map.options.general_center_lat) || 0; 2428 let lon = parseFloat(map.options.general_center_lon) || 0; 2429 let zoom = parseInt(map.options.general_zoom, 10) || 5; 2430 2431 // Create the Leaflet map 2432 const osmMap = L.map(mapElement).setView([lat, lon], zoom); 2433 2434 // Decide which tile layer to use 2435 let tileUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 2436 let tileOpts = { 2437 attribution: "© OpenStreetMap contributors", 2438 maxZoom: 19 2439 }; 2440 switch ( map.options.osm_layer ) { 2441 case 'cyclosm': 2442 tileUrl = "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png"; 2443 tileOpts = { 2444 attribution: "© CyclOSM, OpenStreetMap contributors", 2445 maxZoom: 20 2446 }; 2447 break; 2448 case 'hot': 2449 tileUrl = "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"; 2450 tileOpts = { 2451 attribution: "© Humanitarian OpenStreetMap Team", 2452 maxZoom: 19 2453 }; 2454 break; 2455 case 'standard': 2456 default: 2457 // Use fallback 2458 break; 2459 } 2460 2461 L.tileLayer(tileUrl, tileOpts).addTo(osmMap); 2462 2463 // Optionally disable scroll/drag/zoom if "off" in the data 2464 if (map.options.scrollwheel === 'off') { 2465 osmMap.scrollWheelZoom.disable(); 2466 } 2467 if (map.options.draggable === 'off') { 2468 osmMap.dragging.disable(); 2469 } 2470 if (map.options.double_click_zoom === 'off') { 2471 osmMap.doubleClickZoom.disable(); 2472 } 2473 2474 // Save reference for later usage, e.g. self.get_map() 2475 self.maps[map.map] = osmMap; 2476 self.markers[map.map] = self.markers[map.map] || {}; 2477 mapElement.classList.add('map-full-view'); 2478 2479 // Create marker group based on clustering settings 2480 let markerGroup = self.createOsmMarkerGroup(map.cluster_options, map.cluster_options ? map.cluster_options.cluster : 'off'); 2481 2482 let interactionIcons = {}; 2483 2484 // Add markers from map.markers 2485 _.each(map.markers, function(markerData) {
2486 let leafletMarker = self.buildOsmMarker( markerData, map.options, interactionIcons ); 2487 2488 if ( ! leafletMarker ) { 2489 return; 2490 } 2491 2492 self.markers[map.map][ markerData.marker ] = leafletMarker; 2493 2494 leafletMarker.addTo(markerGroup); 2495 }); 2496 2497 self.registerMarkerInteractions( 2498 map.map, 2499 self.getOsmMarkerInteractions( map.map, interactionIcons ) 2500 ); 2501 2502 // Store the marker group reference for later updates 2503 self.markerGroups[map.map] = markerGroup; 2504 markerGroup.addTo(osmMap); 2505 2506 const markerCount = map.markers.length; 2507 2508 if (markerCount > 1) { 2509 // If fitbounds === 'on', just fit to all markers 2510 if (map.options.fitbounds === 'on') { 2511 osmMap.fitBounds(markerGroup.getBounds(), { padding: [50, 50] }); 2512 } else { 2513 // Otherwise calculate average center 2514 let sumLat = 0; 2515 let sumLon = 0; 2516 let validCount = 0; 2517 2518 _.each(map.markers, function(m) { 2519 let lat = parseFloat(m.markerlat); 2520 let lon = parseFloat(m.markerlon); 2521 if (!isNaN(lat) && !isNaN(lon)) { 2522 sumLat += lat; 2523 sumLon += lon; 2524 validCount++; 2525 } 2526 }); 2527 2528 if (validCount > 0) { 2529 let avgLat = sumLat / validCount; 2530 let avgLon = sumLon / validCount; 2531 // And apply multiple_zoom 2532 let multipleZoom = parseInt(map.options.multiple_zoom, 10) || 5; 2533 osmMap.setView([avgLat, avgLon], multipleZoom); 2534 } 2535 } 2536 } else if (markerCount === 1) { 2537 // Single marker => single_zoom 2538 let singleMarker = map.markers[0]; 2539 let singleLat = parseFloat(singleMarker.markerlat); 2540 let singleLon = parseFloat(singleMarker.markerlon); 2541 let singleZoom = parseInt(map.options.single_zoom, 10) || 6; 2542 osmMap.setView([singleLat, singleLon], singleZoom); 2543 } else { 2544 // Zero markers 2545 osmMap.setView([0, 0], 2); 2546 } 2547 2548 // Trigger an event similar to the other APIs 2549 $(document).trigger('js_event_wpv_addon_maps_init_map_completed', [{ 2550 map_id: map.map, 2551 map_options: map.options 2552 }]); 2553 2554 // Fixes the issues with missing tiles in the map - known OSM issue 2555 setTimeout(function() { 2556 osmMap.invalidateSize(); 2557 }, 500); 2558 }; 2559 2560 /** 2561 * Initialize maps on Elementor widget ready so it works when previewing an Elementor design. 2562 * 2563 * @since 1.5.3 2564 * 2565 * @param {Function} mapInitCallback 2566 */ 2567 self.maybeInitElementorEditorPreviewFix = function( mapInitCallback ) { 2568 if (typeof elementor === 'object' && typeof elementorFrontend === 'object') { 2569 elementorFrontend.hooks.addAction('frontend/element_ready/widget', function ($scope) { 2570 var mapInScope = $scope.find('div.js-wpv-addon-maps-render'); 2571 if (mapInScope.length) { 2572 self.collect_maps_data(); 2573 mapInitCallback(); 2574 } 2575 }); 2576 } 2577 }; 2578 2579 /** 2580 * Initializes a single map by given id, and also cleans up if map with same id was already initialized. 2581 * 2582 * API agnostic. 2583 * 2584 * @since 1.7 2585 * @param {string} id 2586 */ 2587 self.initMapById = function( id, attributes ) { 2588 self.clean_map_data( id ); 2589 2590 const mapData = self.collect_map_data( $( 'div#js-wpv-addon-maps-render-' + id ), attributes ); 2591 2592 // It might happen that this method was called, but the actual map wasn't rendered yet. In that case, it will 2593 // delay itself until the map HTML is ready. 2594 if ( ! mapData.map ) { 2595 _.delay( self.initMapById, 200, id, attributes ); 2596 return; 2597 } 2598 2599 if ( API_GOOGLE === window.toolset_maps_config.provider ) { 2600 self.maybeLazyLoadGoogle( self.init_map_after_loading_styles, mapData ); 2601 } else if ( API_AZURE === window.toolset_maps_config.provider ) { 2602 self.resolveGeolocatedMarkerThenInitMapAzure( mapData ); 2603 } else if ( API_OSM === window.toolset_maps_config.provider ) { 2604 self.initMapOsm( mapData ); 2605 } 2606 }; 2607 2608 /** 2609 * Updates a map instance based on the given ID and attributes 2610 * 2611 * @param {string} id - The unique identifier for the map instance to update 2612 * @param {Object}
2612 attributes - An object containing attributes to update the map with 2613 * 2614 */ 2615 self.updateMapById = function( id, attributes ) { 2616 var mapInstance = self.maps[ id ]; 2617 if ( ! mapInstance ) { 2618 // Map doesn't exist, initialize it 2619 self.initMapById( id, attributes ); 2620 return; 2621 } 2622 2623 // On-the-fly update of the width and height of the map element 2624 const $mapElement = $( 'div#js-wpv-addon-maps-render-' + id ); 2625 if ( attributes.mapWidth ) { 2626 $mapElement.css( 'width', attributes.mapWidth ); 2627 } 2628 if ( attributes.mapHeight ) { 2629 $mapElement.css( 'height', attributes.mapHeight ); 2630 } 2631 2632 var mapData = self.collect_map_data( $( 'div#js-wpv-addon-maps-render-' + id ), attributes ); 2633 if (attributes && mapData.cluster_options) { 2634 mapData.cluster_options.cluster = attributes.mapMarkerClustering ? 'on' : 'off'; 2635 mapData.cluster_options.gridSize = parseInt(attributes.mapMarkerClusteringMinimalDistance) || 60; 2636 mapData.cluster_options.maxZoom = parseInt(attributes.mapMarkerClusteringMaximalZoomLevel) || null; 2637 mapData.cluster_options.zoomOnClick = attributes.mapMarkerClusteringClickZoom !== false; 2638 mapData.cluster_options.minimumClusterSize = parseInt(attributes.mapMarkerClusteringMinimalNumber) || 2; 2639 } 2640 2641 if ( API_GOOGLE === window.toolset_maps_config.provider ) {
2642 self.updateGoogleMapInstance( mapInstance, mapData ); 2643 } else if ( API_AZURE === window.toolset_maps_config.provider ) { 2644 // Ensure atlas is defined before updating 2645 if (typeof atlas === 'undefined' || typeof atlas.DataSource === 'undefined') { 2646 self.maybeLazyLoadAzure(function() { 2647 self.updateAzureMapInstance(mapInstance, mapData); 2648 }); 2649 } else { 2650 self.updateAzureMapInstance(mapInstance, mapData); 2651 } 2652 } else if ( API_OSM === window.toolset_maps_config.provider ) { 2653 self.updateMapOsmInstance( mapInstance, mapData ); 2654 } 2655 }; 2656 2657 /** 2658 * Updates Google map instance with new data 2659 * 2660 * @param {Object} mapInstance Google map instance 2661 * @param {Object} mapData Data for updating the map 2662 * 2663 */ 2664 self.updateGoogleMapInstance = function(mapInstance, mapData) { 2665 2666 // Update map options 2667 var mapOptions = { 2668 center: { 2669 lat: parseFloat( mapData.options.general_center_lat ) || mapInstance.getCenter().lat(), 2670 lng: parseFloat( mapData.options.general_center_lon ) || mapInstance.getCenter().lng() 2671 }, 2672 mapTypeId: mapData.options.map_type || mapInstance.getMapTypeId(), 2673 draggable: mapData.options.draggable !== 'off', 2674 scrollwheel: mapData.options.scrollwheel !== 'off', 2675 disableDoubleClickZoom: mapData.options.double_click_zoom === 'off', 2676 mapTypeControl: mapData.options.map_type_control !== 'off', 2677 fullscreenControl: mapData.options.full_screen_control !== 'off', 2678 zoomControl: mapData.options.zoom_control !== 'off', 2679 // Also toggle the newer combined camera control with zoom (toolsetmaps-608). 2680 cameraControl: mapData.options.zoom_control !== 'off', 2681 streetViewControl: mapData.options.street_view_control !== 'off', 2682 styles: mapData.options.style_json || mapInstance.get('styles'), 2683 backgroundColor: mapData.options.background_color || mapInstance.get( 'backgroundColor' ) 2684 }; 2685 2686 // Function to set map options after styles are loaded 2687 var setMapOptions = function() { 2688 mapInstance.setOptions( mapOptions ); 2689 }; 2690 2691 // Handle styles 2692 if ( mapData.options.styles ) { 2693 mapOptions.styles = mapData.options.styles; 2694 setMapOptions(); 2695 } else if ( mapData.options.style_json ) { 2696 // Load styles from the JSON file 2697 $.getJSON( mapData.options.style_json ) 2698 .done( function( styles ) { 2699 mapData.options.styles = styles; 2700 mapOptions.styles = styles; 2701 } ) 2702 .fail( function() { 2703 console.warn('Failed to load map styles from', mapData.options.style_json); 2704 } ) 2705 .always( function() { 2706 setMapOptions(); 2707 } ); 2708 } else { 2709 mapOptions.styles = mapInstance.get( 'styles' ); 2710 setMapOptions(); 2711 } 2712 2713 // Only update zoom if it has changed and multiple_zoom logic 2714 const currentZoom = mapInstance.getZoom(); 2715 if ( mapData.options.general_zoom !== currentZoom ) { 2716 mapInstance.setZoom( parseInt( mapData.options.general_zoom ) || currentZoom ); 2717 } 2718 2719 // Handle fitBounds for multiple markers 2720 if ( mapData.options.fitbounds === 'on' && Object.keys( self.markers[ mapData.map ] ).length > 1 ) { 2721 const bounds = new google.maps.LatLngBounds(); 2722 Object.values( self.markers[mapData.map] ).forEach( marker => { 2723 bounds.extend( marker.getPosition() ); 2724 }); 2725 mapInstance.fitBounds( bounds ); 2726 } else if ( Object.keys( self.markers[ mapData.map ] ).length > 1 && mapData.options.multiple_zoom ) { 2727 // Apply multiple_zoom if fitbounds is not enabled 2728 mapInstance.setZoom( parseInt( mapData.options.multiple_zoom ) || mapInstance.getZoom() ); 2729 } 2730 2731 // Handle single marker settings (single_center and single_zoom) 2732 if ( Object.keys( self.markers[mapData.map] ).length === 1 ) { 2733 const markerPosition = Object.values( self.markers[mapData.map] )[0].getPosition(); 2734 if ( mapData.options.single_center === 'on' ) { 2735 mapInstance.setCenter( markerPosition ); 2736 } 2737 mapInstance.setZoom( parseInt( mapData.options.single_zoom ) || mapInstance.getZoom() ); 2738 } 2739 2740 // Handle marker clustering if enabled 2741 if ( mapData.options.cluster === 'on' ) { 2742 // Check if MarkerClusterer is available 2743 if ( typeof MarkerClusterer === 'undefined' ) { 2744 console.debug( 'MarkerClusterer library is not loaded. Clustering will not be available.' ); 2745 return; 2746 } 2747 2748 const clusterOptions = {
2749 gridSize: parseInt( mapData.options.clustergridsize ), 2750 maxZoom: parseInt( mapData.options.clustermaxzoom ), 2751 zoomOnClick: mapData.options.clusterclickzoom !== 'off', 2752 minimumClusterSize: parseInt( mapData.options.clusterminsize ) 2753 }; 2754 2755 if ( self.markerClusterer ) { 2756 self.markerClusterer.clearMarkers(); // Clear existing clusters 2757 } 2758 2759 self.markerClusterer = new MarkerClusterer( mapInstance, Object.values( self.markers[ mapData.map ] ), clusterOptions ); 2760 } else if ( self.markerClusterer ) { 2761 // Remove clustering and restore markers to the map 2762 self.markerClusterer.clearMarkers(); 2763 self.markerClusterer.setMap( null ); 2764 self.markerClusterer = null; 2765 2766 // Re-add all markers directly to the map 2767 Object.values( self.markers[ mapData.map ] ).forEach( function( marker ) { 2768 marker.setMap( mapInstance ); 2769 }); 2770 } 2771 2772 // Handle markers 2773 if ( mapData.markers && self.markers[ mapData.map ] ) { 2774 var previous_interactions = self.markerInteractions[ mapData.map ] || {}, 2775 interaction_icons = _.extend( {}, previous_interactions.icons ), 2776 interaction_popups = _.extend( {}, previous_interactions.popups ); 2777 2778 mapData.markers.forEach( function( markerData ) { 2779 var markerId = markerData.marker; 2780 var marker = self.markers[ mapData.map ][ markerId ]; 2781 if ( marker ) { 2782 delete interaction_icons[ markerId ]; 2783 delete interaction_popups[ markerId ]; 2784 2785 // Determine the marker icon and hover icon 2786 var markerIcon = markerData.markericon || mapData.options.marker_icon || ''; 2787 var markerIconHover = markerData.markericonhover || mapData.options.marker_icon_hover || markerIcon; 2788 2789 // Set the marker's title 2790 marker.setTitle( markerData.title || '' ); 2791 2792 // Update the marker's icon 2793 if ( markerIcon ) { 2794 marker.setIcon( self.makeGoogleIcon( markerIcon ) ); 2795 } else { 2796 // Reverts to default marker if markerIcon is empty 2797 marker.setIcon( null ); 2798 } 2799 2800 // Remove existing event listeners to avoid duplicates 2801 google.maps.event.clearListeners( marker, 'mouseover' ); 2802 google.maps.event.clearListeners( marker, 'mouseout' ); 2803 2804 // Add hover events for marker icon swap 2805 if ( 2806 'on' === mapData.options.marker_icon_use_hover 2807 && markerIconHover 2808 && markerIcon !== markerIconHover 2809 ) { 2810 self.bindGoogleMarkerHoverIcons( 2811 marker, 2812 markerId, 2813 self.makeGoogleIcon( markerIcon ), 2814 self.makeGoogleIcon( markerIconHover ), 2815 interaction_icons 2816 ); 2817 } 2818 2819 if ( markerData.markerinfowindow ) { 2820 interaction_popups[ markerId ] = markerData.markerinfowindow; 2821 } 2822 } 2823 }); 2824 2825 self.registerMarkerInteractions( 2826 mapData.map, 2827 self.getGoogleMarkerInteractions( mapData.map, interaction_icons, interaction_popups ) 2828 ); 2829 } 2830 2831 // Handle enabling or disabling Street View mode 2832 if ( mapData.options.street_view === 'on' ) { 2833 const streetViewService = new google.maps.StreetViewService(); 2834 const center = mapInstance.getCenter(); 2835 streetViewService.getPanorama( { location: center, radius: 50 }, function( panoramaData, status ) { 2836 if ( status === google.maps.StreetViewStatus.OK ) { 2837 const streetView = mapInstance.getStreetView(); 2838 streetView.setPosition( panoramaData.location.latLng ); 2839 streetView.setPov( { 2840 heading: parseFloat( mapData.options.heading ) || 0, 2841 pitch: parseFloat( mapData.options.pitch ) || 0 2842 } ); 2843 streetView.setVisible( true ); 2844 } else { 2845 console.warn( 'Street View data not found for this location.' ); 2846 const streetView = mapInstance.getStreetView(); 2847 streetView.setVisible( false ); 2848 } 2849 }); 2850 } else { 2851 const streetView = mapInstance.getStreetView(); 2852 streetView.setVisible( false ); 2853 } 2854 }; 2855 2856 /** 2857 * Updates Azure map instance with new data and markers, keeping the previous zoom level. 2858 * 2859 * @param {Object} mapInstance Azure map instance 2860 * @param {Object} mapData Data for updating the map 2861 * 2862 */ 2863 self.updateAzureMapInstance = function( mapInstance, mapData ) { 2864 2865 // Capture the current zoom level before reinitializing the map 2866 let previousZoom = mapInstance.getCamera().zoom; 2867 2868 // Re-initialize the map and capture the new instance 2869 let newMapInstance = self.initMapAzure( mapData ); 2870 2871 // Restore the previous zoom level if necessary 2872 newMapInstance.setCamera( { 2873 zoom: previousZoom 2874 } ); 2875 2876 // Use DOM elements to gather markers
2877 let thiz_map_id = mapData.map; 2878 let thiz_map_points = []; 2879 2880 // Iterate over each marker DOM element associated with this map ID 2881 $( '.js-wpv-addon-maps-markerfor-' + thiz_map_id ).each( function() { 2882 let thiz_marker = $( this ); 2883 2884 // Collect marker data from the DOM element 2885 let markerLat = parseFloat( thiz_marker.data('markerlat') ); 2886 let markerLon = parseFloat( thiz_marker.data('markerlon') ); 2887 2888 // Skip markers without valid coordinates 2889 if ( !markerLat || !markerLon || markerLat === 'geo' ) { 2890 // Skipping marker due to invalid lat/lon or geo position 2891 return true; 2892 } 2893 2894 thiz_map_points.push( { 2895 'marker': thiz_marker.data( 'marker' ), 2896 'title': self.getPlainTextTitle( thiz_marker.attr( 'data-markertitle' ) ), 2897 'markerlat': markerLat, 2898 'markerlon': markerLon, 2899 'markerinfowindow': thiz_marker.html(), 2900 'markericon': thiz_marker.data( 'markericon' ), 2901 'markericonhover': thiz_marker.data( 'markericonhover' ) 2902 } ); 2903 }); 2904 2905 // Update mapData with collected markers 2906 mapData.markers = thiz_map_points; 2907 2908 self.initMapAzure( mapData ); 2909 2910 // Update markers 2911 if ( mapData.markers.length > 0 ) { 2912 let bounds = new atlas.data.BoundingBox.fromPositions( 2913 mapData.markers.map( marker => [ parseFloat( marker.markerlon ), parseFloat( marker.markerlat ) ] ) 2914 ); 2915 2916 mapData.markers.forEach( ( marker ) => { 2917 if ( marker.markerlat && marker.markerlon ) { 2918 // Define the marker icon with hover and normal icons 2919 let icon = marker.markericon || ''; 2920 let iconHover = marker.markericonhover || icon; 2921 2922 let mapMarker = new atlas.HtmlMarker({ 2923 position: [ marker.markerlon, marker.markerlat ], 2924 text: marker.title || 'Marker', 2925 htmlContent: `<img src="${icon}" class="marker-icon" />` 2926 }); 2927 2928 // Add marker to the map 2929 mapInstance.markers.add( mapMarker ); 2930 2931 // Add hover effect 2932 mapMarker.getElement().addEventListener( 'mouseover', function() { 2933 mapMarker.setOptions( { 2934 htmlContent: `<img src="${iconHover}" class="marker-icon-hover" />` 2935 } ); 2936 }); 2937 2938 mapMarker.getElement().addEventListener( 'mouseout', function() { 2939 mapMarker.setOptions( { 2940 htmlContent: `<img src="${icon}" class="marker-icon" />` 2941 } ); 2942 }); 2943 } 2944 }); 2945 2946 // Handle zoom for one marker or multiple markers 2947 if ( mapData.markers.length === 1 ) { 2948 // Single marker - use a predefined zoom level 2949 let singleMarker = mapData.markers[0]; 2950 mapInstance.setCamera( { 2951 center: [ singleMarker.markerlon, singleMarker.markerlat ], 2952 zoom: mapData.options.single_zoom || 15 2953 } ); 2954 } else if ( bounds && bounds.southWest && bounds.northEast ) { 2955 // Multiple markers - fit bounds 2956 mapInstance.setCamera( { 2957 bounds: bounds, 2958 padding: 20 2959 } ); 2960 } else { 2961 // Invalid bounds - setting default center and zoom 2962 mapInstance.setCamera( { 2963 center: [ parseFloat( mapData.options.general_center_lon ), parseFloat( mapData.options.general_center_lat ) ], 2964 zoom: mapData.options.general_zoom || 10 2965 }); 2966 } 2967 } 2968 self.resolveGeolocatedMarkerThenInitMapAzure( mapData ); 2969 }; 2970 2971 /** 2972 * Updates Leaflet (OSM) map instance with new data 2973 * 2974 * @param {Object} mapInstance Leaflet map instance 2975 * @param {Object} mapData Data for updating the map 2976 */ 2977 self.updateMapOsmInstance = function( mapInstance, mapData ) { 2978 2979 // Remove previous tile layers 2980 mapInstance.eachLayer( function( layer ) { 2981 if ( layer instanceof L.TileLayer ) { 2982 mapInstance.removeLayer( layer ); 2983 } 2984 } ); 2985 2986 // Choose which tile layer to use 2987 let tileUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; // fallback 2988 let tileOpts = { 2989 attribution: "© OpenStreetMap contributors", 2990 maxZoom: 19 2991 }; 2992 2993 switch ( mapData.options.osm_layer ) { 2994 case 'cyclosm': 2995 tileUrl = "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png"; 2996 tileOpts = { 2997 attribution: "© CyclOSM, OpenStreetMap contributors", 2998 maxZoom: 20 2999 }; 3000 break; 3001 case 'hot': 3002 tileUrl = "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"; 3003 tileOpts = { 3004 attribution: "© Humanitarian OpenStreetMap Team", 3005 maxZoom: 19 3006 }; 3007 break; 3008 case 'standard': 3009 default: 3010 // use fallback 3011 break; 3012 } 3013 3014 // Set the new tile layer 3015 L.tileLayer( tileUrl, tileOpts ).addTo( mapInstance ); 3016 3017 // Update map center and zoom
3018 if ( mapData.options.general_center_lat && mapData.options.general_center_lon ) { 3019 var newCenter = [ parseFloat( mapData.options.general_center_lat ), parseFloat( mapData.options.general_center_lon ) ]; 3020 mapInstance.setView( newCenter, parseInt( mapData.options.general_zoom, 10 ) || mapInstance.getZoom() ); 3021 } 3022 3023 // Optionally disable scroll/drag/zoom based on new options 3024 if ( mapData.options.scrollwheel === 'off' ) { 3025 mapInstance.scrollWheelZoom.disable(); 3026 } else { 3027 mapInstance.scrollWheelZoom.enable(); 3028 } 3029 3030 if ( mapData.options.draggable === 'off' ) { 3031 mapInstance.dragging.disable(); 3032 } else { 3033 mapInstance.dragging.enable(); 3034 } 3035 3036 if ( mapData.options.double_click_zoom === 'off' ) { 3037 mapInstance.doubleClickZoom.disable(); 3038 } else { 3039 mapInstance.doubleClickZoom.enable(); 3040 } 3041 3042 // Update markers 3043 if ( self.markers[mapData.map] ) { 3044 Object.keys( self.markers[mapData.map] ).forEach( function( markerId ) { 3045 var oldMarker = self.markers[ mapData.map ][ markerId ]; 3046 mapInstance.removeLayer( oldMarker ); 3047 }); 3048 } 3049 3050 // Remove the existing marker group (if any) 3051 if ( self.markerGroups[mapData.map] ) { 3052 mapInstance.removeLayer( self.markerGroups[mapData.map] ); 3053 delete self.markerGroups[mapData.map]; 3054 } 3055 3056 // Reset self.markers[mapData.map] to an empty object 3057 self.markers[ mapData.map ] = {}; 3058 3059 // Add new markers based on clustering settings 3060 let markerGroup = self.createOsmMarkerGroup(mapData.cluster_options, mapData.cluster_options ? mapData.cluster_options.cluster : 'off'); 3061 3062 let interactionIcons = {}; 3063 3064 _.each( mapData.markers, function( markerData ) {
3065 let leafletMarker = self.buildOsmMarker( markerData, mapData.options, interactionIcons ); 3066 3067 if ( ! leafletMarker ) { 3068 console.warn( `Invalid marker coordinates: lat=${markerData.markerlat}, lon=${markerData.markerlon}` ); 3069 return; 3070 } 3071 3072 self.markers[ mapData.map ][ markerData.marker ] = leafletMarker; 3073 3074 leafletMarker.addTo( markerGroup ); 3075 }); 3076 3077 self.registerMarkerInteractions( 3078 mapData.map, 3079 self.getOsmMarkerInteractions( mapData.map, interactionIcons ) 3080 ); 3081 3082 // Store the marker group reference for later updates 3083 self.markerGroups[mapData.map] = markerGroup; 3084 markerGroup.addTo( mapInstance ); 3085 3086 // Fit bounds if necessary 3087 const markerCount = mapData.markers.length; 3088 3089 if ( markerCount > 1 ) { 3090 // If fitbounds is on - do fitBounds 3091 if ( mapData.options.fitbounds === 'on' ) { 3092 mapInstance.fitBounds( markerGroup.getBounds(), { padding: [50, 50] } ); 3093 } else { 3094 // Calculate average center and use multiple_zoom 3095 let sumLat = 0; 3096 let sumLon = 0; 3097 let validCount = 0; 3098 3099 _.each(mapData.markers, function( m ) { 3100 let lat = parseFloat( m.markerlat ); 3101 let lon = parseFloat( m.markerlon ); 3102 if ( !isNaN( lat ) && !isNaN( lon) ) { 3103 sumLat += lat; 3104 sumLon += lon; 3105 validCount++; 3106 } 3107 }); 3108 3109 if ( validCount > 0 ) { 3110 let avgLat = sumLat / validCount; 3111 let avgLon = sumLon / validCount; 3112 let multipleZoom = parseInt( mapData.options.multiple_zoom, 10 ) || mapInstance.getZoom(); 3113 mapInstance.setView( [avgLat, avgLon], multipleZoom ); 3114 } 3115 } 3116 } else if (markerCount === 1) { 3117 // Single marker => single_zoom 3118 let singleMarker = mapData.markers[ 0 ]; 3119 let singleLat = parseFloat( singleMarker.markerlat ); 3120 let singleLon = parseFloat( singleMarker.markerlon ); 3121 let singleZoom = parseInt( mapData.options.single_zoom, 10 ) || mapInstance.getZoom(); 3122 mapInstance.setView( [singleLat, singleLon], singleZoom ); 3123 } else { 3124 // Zero markers => fallback 3125 mapInstance.setView( [0,0], 2 ); 3126 } 3127 3128 // Trigger event 3129 $(document).trigger( 'js_event_wpv_addon_maps_init_map_completed', [ { 3130 map_id: mapData.map, 3131 map_options: mapData.options 3132 } ] ); 3133 3134 // Fixes the issues with missing tiles in the map - known OSM issue 3135 setTimeout(function() { 3136 mapInstance.invalidateSize(); 3137 }, 500); 3138 3139 }; 3140 3141 /** 3142 * Lazy load Google libraries and other libraries depending on it, if needed. 3143 * @param {Function} initMap 3144 * @param {object} mapData Callback parameter. 3145 */ 3146 self.maybeLazyLoadGoogle = function( initMap = null, mapData = null ) { 3147 if ( self.api === null ) { 3148 $.getScript( 3149 `https://maps.googleapis.com/maps/api/js?libraries=places&v=3&key=${ window.toolset_maps_config.googleBrowserKey }&callback=Function.prototype` 3150 ) 3151 .done( function() { 3152 self.api = window.google.maps; 3153 self.initGoogle(); // Do full init, because only now we have the API ready. 3154 } ) 3155 .fail( function() { 3156 self.api = false; 3157 console.warn( 'Toolset Maps: failed to lazy load Google Maps library.' ); 3158 } ); 3159 } else { 3160 initMap( mapData ); 3161 // This is emitted only by collect_maps_data, not by the single map data collector that's used here, and is 3162 // needed by add_current_visitor_location_after_geolocation in order to run at the right time. (Google API 3163 // only). 3164 $( document ).trigger('js_event_wpv_addon_maps_map_data_collected'); 3165 } 3166 } 3167 3168 /** 3169 * Observes the DOM for changes and initializes the appropriate map API 3170 * (Google Maps or Azure Maps) once map data becomes available. 3171 * 3172 * This function sets up a MutationObserver on the document body to monitor 3173 * for any additions or modifications. When map data is detected, it 3174 * collects the data, stops observing further changes, and initializes 3175 * the selected map API based on the configuration. 3176 * 3177 */ 3178 self.waitForMapData = function() { 3179 const targetNode = document.body; 3180 const config = { childList: true, subtree: true }; 3181 3182 const callback = function( mutationsList, observer ) { 3183 self.collect_maps_data(); 3184 if ( self.maps_data.length > 0 ) { 3185 observer.disconnect(); 3186 if (API_GOOGLE === window.toolset_maps_config.provider) { 3187 self.initGoogle(); 3188 } else if (API_AZURE === window.toolset_maps_config.provider) { 3189 self.initAzure(); 3190 } else if (API_OSM === window.toolset_maps_config.provider) { 3191 self.initOsm(); 3192 } 3193 } 3194 }; 3195 3196 const observer = new MutationObserver( callback ); 3197 observer.observe( targetNode, config ); 3198 }; 3199 3200 3201 self.init = function() { 3202 self.collect_maps_data(); 3203 // If no map data collected yet, set up observer 3204 if ( self.maps_data.length === 0 ) { 3205 self.waitForMapData(); 3206 return; 3207 } 3208 3209 if (API_GOOGLE === window.toolset_maps_config.provider) { 3210 self.initGoogle(); 3211 } else if (API_AZURE === window.toolset_maps_config.provider) { 3212 self.initAzure(); 3213 } else if (API_OSM === window.toolset_maps_config.provider) { 3214 self.initOsm(); 3215 } 3216 }; 3217 3218 self.init(); 3219}; 3220 3221jQuery( function( $ ) {
3222 WPViews.view_addon_maps = new WPViews.ViewAddonMaps( $ ); 3223});
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.