PageSourceSearch

https://premierhighschools.com/wp-content/plugins/toolset-maps/resources/js/wpv_addon_maps.js?ver=2.3.0

js premierhighschools.com collected 2026-10-02 14:59:58 UTC 106,508 bytes, 3,223 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.