PageSourceSearch

https://www.brightapartments.com/wp-content/plugins/toolset-maps/resources/js/wpv_addon_maps.js?ver=2.2.4

js brightapartments.com collected 2026-10-06 22:30:14 UTC 101,038 bytes, 2,895 lines download raw bytes

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

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