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