PageSourceSearch

https://www.fpch.org/modules/toolset-maps/resources/js/wpv_addon_maps.js

js fpch.org collected 2026-09-26 16:49:05 UTC 48,962 bytes, 1,580 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
9	const self = this;
10
11	/** @var {Object} views_addon_maps_i10n */
12
13	if ( API_GOOGLE === views_addon_maps_i10n.api_used ) {
14		if ( typeof google === 'undefined' || typeof google.maps === 'undefined' ) {
15			self.api = null;
16		} else {
17			/** @var {Object} google */
18			self.api = google.maps;
19		}
20	} else {
21		/** @var {Object} atlas */
22		self.api = null;
23	}
24
25	self.maps_data = [];
26	self.maps = {};
27	self.markers = {};
28	self.infowindows = {};
29	self.bounds = {};
30	self.view_map_blocks_relation = {};
31
32	self.default_cluster_options = {
33		imagePath:			views_addon_maps_i10n.cluster_default_imagePath,
34		gridSize:			60,
35		maxZoom:			null,
36		zoomOnClick:		true,
37		minimumClusterSize:	2
38	};
39
40	self.cluster_options = {};
41	self.has_cluster_options = {};
42
43	self.resize_queue = [];
44
45	/**
46	* collect_maps_data
47	*
48	* Before init_maps
49	*
50	* @since 1.0
51	*/
52
53	self.collect_maps_data = function() {
54		$( '.js-wpv-addon-maps-render' ).each( function() {
55			self.collect_map_data( $( this ) );
56		});
57
58		$( document ).trigger('js_event_wpv_addon_maps_map_data_collected');
59	};
60
61	/**
62	 * Initializes street view mode on given map.
63	 * @param Object map
64	 * @param latlon
65	 * @since 1.5
66	 */
67	self.initStreetView = function( map, latlon ) {
68		var streetViewService = new google.maps.StreetViewService();
69		var panorama = self.maps[ map.map_id ].getStreetView();
70
71		streetViewService.getPanoramaByLocation( latlon, 100, function (streetViewPanoramaData, status) {
72
73			if ( status === google.maps.StreetViewStatus.OK ) {
74				var streetLatlon = streetViewPanoramaData.location.latLng;
75				var heading = ( map.map_options.heading !== '' )
76					? map.map_options.heading
77					: google.maps.geometry.spherical.computeHeading( streetLatlon, latlon);
78				var pitch = ( map.map_options.pitch !== '' ) ? map.map_options.pitch : 0;
79
80				panorama.setPosition( streetLatlon );
81				panorama.setPov({
82					heading: heading,
83					pitch: pitch
84				});
85				panorama.setVisible( true );
86			}
87		});
88	};
89
90	/**
91	 * Sets a trigger to init street view after map is ready. Namespaces trigger so we do this only once per map.
92	 * @param String mapId
93	 * @param jQuery $marker
94	 * @listens js_event_wpv_addon_maps_init_map_completed
95	 * @since 1.5
96	 */
97	self.waitForMapInitThenInitStreetView = function( mapId, $marker ) {
98		$( document ).on( 'js_event_wpv_addon_maps_init_map_completed', function( event, map ) {
99			if ( mapId === map.map_id ){
100				var latlon = new google.maps.LatLng( $marker.data('markerlat'), $marker.data('markerlon') );
101				self.initStreetView( map, latlon );
102			}
103		} );
104	};
105
106	/**
107	 * collect_map_data
108	 *
109	 * @since 1.0
110	 * @since 1.5 handle Street Views
111	 */
112	self.collect_map_data = function( thiz_map ) {
113		var thiz_map_id = thiz_map.data( 'map' ),
114		thiz_map_points = [],
115		thiz_map_options = {};
116		var streetViewMarkerFound = false;
117
118		$( '.js-wpv-addon-maps-markerfor-' + thiz_map_id ).each( function() {
119			var thiz_marker = $( this );
120			// Handle special case when we don't have coordinates, but instead need to ask browser for current users
121			// position (only for Google API)
122			if (
123				thiz_marker.data( 'markerlat' ) === 'geo'
124				&& API_GOOGLE === views_addon_maps_i10n.api_used
125			) {
126				// In case map render wait is requested by a marker, add that data to map
127				if (thiz_marker.data( 'markerlon' ) === 'wait') {
128					thiz_map_options['render'] = 'wait';
129					self.add_current_visitor_location_after_geolocation(thiz_map_id, thiz_marker);
130				} else {
131					self.add_current_visitor_location_after_init(thiz_map_id, thiz_marker);
132				}
133				return true;
134			}
135
136			// Handle Street View marker as special case (only when using Google API)
137			if (
138				thiz_map.data('streetview') === 'on'
139				&& API_GOOGLE === views_addon_maps_i10n.api_used
140			) {
141				if ( thiz_map.data('markerid') === thiz_marker.data('marker') ) {
142					self.waitForMapInitThenInitStreetView( thiz_map_id, thiz_marker );
143					streetViewMarkerFound = true;
144					return true;
145				} else if ( thiz_map.data('location') === 'first' && !streetViewMarkerFound ) {
146					self.waitForMapInitThenInitStreetView( thiz_map_id, thiz_marker );
147					streetViewMarkerFound = true;
148					return true;
149				}
150			}
151
152			thiz_map_points.push(
153				{
154					'marker': thiz_marker.data('marker'),
155					'title': thiz_marker.data('markertitle'),
156					'markerlat': thiz_marker.data('markerlat'),
157					'markerlon': thiz_marker.data('markerlon'),
158					'markerinfowindow': thiz_marker.html(),
159					'markericon': thiz_marker.data('markericon'),
160					'markericonhover': thiz_marker.data('markericonhover')
161				}
162			);
163
164		});
165
166		// Some error catching when street view from marker requested and marker not found
167		if (
168			(
169				thiz_map.data('location') === 'first'
170				|| thiz_map.data('markerid')
171			)
172			&& !streetViewMarkerFound
173		) {
174			console.warn( views_addon_maps_i10n.marker_not_found_warning, thiz_map_id );
175		}
176
177		thiz_map_options['general_zoom'] = thiz_map.data( 'generalzoom' );
178		thiz_map_options['general_center_lat'] = thiz_map.data( 'generalcenterlat' );
179		thiz_map_options['general_center_lon'] = thiz_map.data( 'generalcenterlon' );
180		thiz_map_options['fitbounds'] = thiz_map.data( 'fitbounds' );
181		thiz_map_options['single_zoom'] = thiz_map.data( 'singlezoom' );
182		thiz_map_options['single_center'] = thiz_map.data( 'singlecenter' );
183		thiz_map_options['map_type'] = thiz_map.data( 'maptype' );
184		thiz_map_options['show_layer_interests'] = thiz_map.data( 'showlayerinterests' );
185		thiz_map_options['marker_icon'] = thiz_map.data( 'markericon' );
186		thiz_map_options['marker_icon_hover'] = thiz_map.data( 'markericonhover' );
187		thiz_map_options['draggable'] = thiz_map.data( 'draggable' );
188		thiz_map_options['scrollwheel'] = thiz_map.data( 'scrollwheel' );
189		thiz_map_options['double_click_zoom'] = thiz_map.data( 'doubleclickzoom' );
190		thiz_map_options['map_type_control'] = thiz_map.data( 'maptypecontrol' );
191		thiz_map_options['full_screen_control'] = thiz_map.data( 'fullscreencontrol' );
192		thiz_map_options['zoom_control'] = thiz_map.data( 'zoomcontrol' );
193		thiz_map_options['street_view_control'] = thiz_map.data( 'streetviewcontrol' );
194		thiz_map_options['background_color'] = thiz_map.data( 'backgroundcolor' );
195		thiz_map_options['cluster'] = thiz_map.data( 'cluster' );
196		thiz_map_options['style_json'] = thiz_map.data( 'stylejson' );
197		thiz_map_options['spiderfy'] = thiz_map.data( 'spiderfy' );
198		thiz_map_options['lat'] = thiz_map.data( 'lat' );
199		thiz_map_options['long'] = thiz_map.data( 'long' );
200		thiz_map_options['heading'] = thiz_map.data( 'heading' );
201		thiz_map_options['pitch'] = thiz_map.data( 'pitch' );
202
203		var thiz_cluster_options = {
204			'cluster':				thiz_map.data( 'cluster' ),
205			'gridSize':				parseInt( thiz_map.data( 'clustergridsize' ) ),
206			'maxZoom':				( parseInt( thiz_map.data( 'clustermaxzoom' ) ) > 0 ) ? parseInt( thiz_map.data( 'clustermaxzoom' ) ) : null,
207			'zoomOnClick':			( thiz_map.data( 'clusterclickzoom' ) == 'off' ) ? false : true,
208			'minimumClusterSize':	parseInt( thiz_map.data( 'clusterminsize' ) )
209		};
210
211		// 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
212		if ( _.has( self.has_cluster_options, thiz_map_id ) ) {
213			if ( _.has( self.has_cluster_options[ thiz_map_id ] , "styles" ) ) {
214				thiz_cluster_options['styles'] = self.has_cluster_options[ thiz_map_id ]['styles'];
215			}
216			if ( _.has( self.has_cluster_options[ thiz_map_id ] , "calculator" ) ) {
217				thiz_cluster_options['calculator'] = self.has_cluster_options[ thiz_map_id ]['calculator'];
218			}
219		}
220
221		var thiz_map_collected = {
222			'map':				thiz_map_id,
223			'markers':			thiz_map_points,
224			'options':			thiz_map_options,
225			'cluster_options':	thiz_cluster_options
226		};
227
228		self.maps_data.push( thiz_map_collected );
229		self.cluster_options[ thiz_map_id ] = thiz_cluster_options;
230
231		return thiz_map_collected;
232
233	};
234
235	/**
236	 * Gets current visitor location from browser, then adds the marker to given map.
237	 *
238	 * If location fetching failed, render the map without it...
239	 *
240	 * @since 1.4
241	 * @param {String} thiz_map_id
242	 * @param {jQuery} thiz_marker
243	 */
244	self.add_current_visitor_location = function(thiz_map_id, thiz_marker) {
245		var map_key = self.get_map_key_by_id(thiz_map_id);
246		if (navigator.geolocation) {
247			navigator.geolocation.getCurrentPosition(
248				function (position) {
249					// Add data to collection and ask map to redraw
250					self.maps_data[map_key].markers.push({
251						'marker': thiz_marker.data('marker'),
252						'title': thiz_marker.data('markertitle'),
253						'markerlat': position.coords.latitude,
254						'markerlon': position.coords.longitude,
255						'markerinfowindow': thiz_marker.html(),
256						'markericon': thiz_marker.data('markericon'),
257						'markericonhover': thiz_marker.data('markericonhover')
258					});
259
260					self.init_map_after_loading_styles(self.maps_data[map_key]);
261				},
262				function (position_error) {
263					self.init_map_after_loading_styles(self.maps_data[map_key]);
264				}
265			);
266		} else {
267			self.init_map_after_loading_styles(self.maps_data[map_key]);
268		}
269	};
270
271	/**
272	 * Wraps waiting for map to render first time before adding browser location marker
273	 * @since 1.4
274	 * @param {String} thiz_map_id
275	 * @param {jQuery} thiz_marker
276	 */
277	self.add_current_visitor_location_after_init = function(thiz_map_id, thiz_marker) {
278		$( document ).on( 'js_event_wpv_addon_maps_init_map_completed.'+thiz_map_id, function( event, data ) {
279			if (thiz_map_id === data.map_id) {
280				// Stop listening to this event for this map (because the event can fire multiple times
281				// and we really need to add marker only once)
282				$( document ).off( 'js_event_wpv_addon_maps_init_map_completed.'+thiz_map_id );
283
284				self.add_current_visitor_location(thiz_map_id, thiz_marker);
285			}
286		});
287	};
288
289	/**
290	 * Wraps waiting for geolocation before rendering map.
291	 *
292	 * Waits for all map data to be ready before trying to render it. (Problems could otherwise happen when geolocation
293	 * is already approved and map data is not yet collected.)
294	 *
295	 * @since 1.4
296	 * @param {String} thiz_map_id
297	 * @param {jQuery} thiz_marker
298	 */
299	self.add_current_visitor_location_after_geolocation = function(thiz_map_id, thiz_marker) {
300		$( document ).one('js_event_wpv_addon_maps_map_data_collected.'+thiz_map_id, function() {
301			self.add_current_visitor_location(thiz_map_id, thiz_marker);
302		});
303	};
304
305	/**
306	 * Given map id string returns array key for a map in maps_data
307	 * @since 1.4
308	 * @param {String} map_id
309	 * @returns {Number}
310	 */
311	self.get_map_key_by_id = function(map_id) {
312		return _.findLastIndex(self.maps_data, { map: map_id });
313	};
314
315	/**
316	 * Checks if json style needed, loads JSON file with map styles and then inits the map
317	 * @since 1.4
318	 * @param {Object} map
319	 */
320	self.init_map_after_loading_styles = function( map ) {
321		if ( map.options.style_json ) {
322			$.getJSON( map.options.style_json )
323				.done( function( styles ) {
324					map.options.styles = styles;
325				} )
326				.always( function () {
327					// Even if styles loading failed, map can be rendered with standard style, so do it always.
328					self.initMapOrWaitIfInsideHiddenBootstrapAccordionOrTab( map );
329				} );
330		} else {
331			self.initMapOrWaitIfInsideHiddenBootstrapAccordionOrTab( map );
332		}
333	};
334
335	/**
336	 * Checks if this map is inside a hidden (collapsed) Bootstrap accordion.
337	 *
338	 * @param {String} map_selector
339	 *
340	 * @return {String} Returns accordion id if found, empty string otherwise
341	 *
342	 * @since 1.4.2
343	 */
344	self.check_being_inside_collapsed_bootstrap_accordion = function( map_selector ) {
345		var accordion_id = '';
346		var toggle_href = '';
347
348		// Find all accordion toggles which are currently collapsed, then find which of the accordions contains our map
349		// and return accordion id. If none, we'll return empty string.
350		$('a[data-toggle="collapse"].collapsed').each( function( index, toggle ) {
351			toggle_href = $( toggle ).attr('href');
352
353			if ( $( toggle_href ).has( map_selector ).length ) {
354				accordion_id = toggle_href;
355				return false;
356			}
357		} );
358
359		return accordion_id;
360	};
361
362	/**
363	 * Checks if this map is inside a hidden Bootstrap tab.
364	 *
365	 * @param {String} map_selector
366	 *
367	 * @return {string}
368	 *
369	 * @since 1.4.2
370	 */
371	self.check_being_inside_hidden_bootstrap_tab = function( map_selector ) {
372		var tab_toggle = '';
373		var toggle_href = '';
374		var $tab_body;
375
376		$('a[data-toggle="tab"]').each( function( index, toggle ) {
377			toggle_href = $( toggle ).attr('href');
378			$tab_body = $( toggle_href );
379
380			if ( !$tab_body.hasClass('active') && $tab_body.has( map_selector ).length ) {
381				tab_toggle = toggle;
382				return false;
383			}
384		} );
385
386		return tab_toggle;
387	};
388
389	/**
390	 * If not inside hidden Bootstrap accordion or tab, init map, if inside, hook to shown event to init then.
391	 *
392	 * @param {Object} map
393	 *
394	 * @since 1.4.2
395	 */
396	self.initMapOrWaitIfInsideHiddenBootstrapAccordionOrTab = function(map ) {
397		var map_selector = '#js-wpv-addon-maps-render-' + map.map;
398		var accordion_id = self.check_being_inside_collapsed_bootstrap_accordion( map_selector );
399		var tab_toggle = self.check_being_inside_hidden_bootstrap_tab( map_selector );
400
401		if ( accordion_id ) {
402			$( accordion_id ).one('shown.bs.collapse', function() {
403				self.init_map( map );
404			} );
405		} else if( tab_toggle ) {
406			$( tab_toggle ).one('shown.bs.tab', function() {
407				self.init_map( map );
408			} );
409		} else {
410			self.init_map( map );
411		}
412	};
413
414	/**
415	* init_maps
416	*
417	* @since 1.0
418	*/
419
420	self.init_maps = function() {
421		self.maps_data.map( function( map ) {
422			// If there is a marker on the map with option for map rendering to wait until it's ready, do nothing.
423			// (Marker itself will trigger map rendering when ready.)
424			if (
425				map.options.render
426				&& map.options.render === 'wait'
427			) {
428				return true;
429			}
430
431			// Handle other maps
432			self.init_map_after_loading_styles( map );
433		});
434	};
435
436	/**
437	* init_map
438	*
439	* @since 1.0
440	*/
441
442	self.init_map = function( map ) {
443		var map_icon = '',
444		map_icon_hover = '',
445		map_settings = {
446			zoom: map.options['general_zoom']
447		},
448		event_settings = {
449			map_id:			map.map,
450			map_options:	map.options
451		};
452		var spiderfy = false;
453		var clickEvent = 'click';
454
455		$( document ).trigger( 'js_event_wpv_addon_maps_init_map_started', [ event_settings ] );
456
457		if (
458			map.options['general_center_lat'] != ''
459			&& map.options['general_center_lon'] != ''
460		) {
461			map_settings['center'] = {
462				lat: map.options['general_center_lat'],
463				lng: map.options['general_center_lon']
464			};
465		} else {
466			map_settings['center'] = {
467				lat: 0,
468				lng: 0
469			};
470		}
471
472		if ( map.options['draggable'] == 'off' ) {
473			map_settings['draggable'] = false;
474		}
475
476		if ( map.options['scrollwheel'] == 'off' ) {
477			map_settings['scrollwheel'] = false;
478		}
479
480		if ( map.options['double_click_zoom'] == 'off' ) {
481			map_settings['disableDoubleClickZoom'] = true;
482		}
483
484		if ( map.options['map_type_control'] == 'off' ) {
485			map_settings['mapTypeControl'] = false;
486		}
487
488		if ( map.options['full_screen_control'] == 'off' ) {
489			map_settings['fullscreenControl'] = false;
490		}
491
492		if ( map.options['zoom_control'] == 'off' ) {
493			map_settings['zoomControl'] = false;
494		}
495
496		if ( map.options['street_view_control'] == 'off' ) {
497			map_settings['streetViewControl'] = false;
498		}
499
500		if ( map.options['background_color'] != '' ) {
501			map_settings['backgroundColor'] = map.options['background_color'];
502		}
503
504		if ( map.options['styles'] ) {
505			map_settings['styles'] = map.options['styles'];
506		}
507
508		if ( map.options['map_type'] ) {
509			map_settings['mapTypeId'] = map.options['map_type'];
510		}
511
512		const mapElement = document.getElementById( 'js-wpv-addon-maps-render-' + map.map );
513
514		// In Gutenberg editor, it might happen that the map gets refreshed a couple of times and the div is missing at
515		// this moment. Just give up right now, it will get rendered later on.
516		if ( ! mapElement ) return;
517
518		self.maps[ map.map ] = new self.api.Map( mapElement, map_settings );
519
520		$( document ).trigger( 'js_event_wpv_addon_maps_init_map_inited', [ event_settings ] );
521
522		self.bounds[ map.map ] = new self.api.LatLngBounds();
523
524		if ( map.options['marker_icon'] != '' ) {
525			map_icon = map.options['marker_icon'];
526		}
527
528		if ( map.options['marker_icon_hover'] != '' ) {
529			map_icon_hover = map.options['marker_icon_hover'];
530		}
531
532		if ( 'on' === map.options['spiderfy'] ) {
533			var oms = new OverlappingMarkerSpiderfier( self.maps[ map.map ], {
534				markersWontMove: true,
535				markersWontHide: true,
536				basicFormatEvents: true
537			});
538			spiderfy = true;
539		}
540
541		map.markers.map( function( marker ) {
542			var marker_lat_long = new self.api.LatLng( marker.markerlat, marker.markerlon ),
543			marker_map_icon = ( marker.markericon == '' ) ? map_icon : marker.markericon,
544			marker_map_icon_hover = ( marker.markericonhover == '' ) ? map_icon_hover : marker.markericonhover,
545			marker_settings = {
546				position: marker_lat_long,
547				optimized: false
548			};
549
550			// 'default' is just used so that default marker icon doesn't get replaced with global marker icon. Now set
551			// it empty, so the rest of code knows how to handle it.
552			if ( marker_map_icon === 'default' ) {
553				marker_map_icon = '';
554			}
555
556			if ( spiderfy ) {
557				clickEvent = 'spider_click';
558			} else {
559				marker_settings.map = self.maps[ map.map ]
560				clickEvent = 'click';
561			}
562
563			// Helps SVG marker icons to have a reasonable size and render properly in Internet Explorer
564			if ( marker_map_icon.slice( -4 ).toLowerCase() === '.svg' ) {
565				var scaledSize = new google.maps.Size(32, 32);
566			} else {
567				var scaledSize = null;
568			}
569
570			if ( marker_map_icon != '' ) {
571				marker_settings['icon'] = {
572					url: marker_map_icon,
573					scaledSize: scaledSize,
574				}
575			}
576			if ( marker.title != '' ) {
577				marker_settings['title'] = marker.title;
578			}
579
580			self.markers[ map.map ] = self.markers[ map.map ] || {};
581
582			self.markers[ map.map ][ marker.marker ] = new self.api.Marker(marker_settings);
583
584			self.bounds[ map.map ].extend( self.markers[ map.map ][ marker.marker ].position );
585
586			if (
587				marker_map_icon != ''
588				|| marker_map_icon_hover != ''
589			) {
590				marker_map_icon = ( marker_map_icon == '' ) ? views_addon_maps_i10n.marker_default_url : marker_map_icon;
591				marker_map_icon_hover = ( marker_map_icon_hover == '' ) ? marker_map_icon : marker_map_icon_hover;
592
593				if ( marker_map_icon != marker_map_icon_hover ) {
594					var marker_icon_scaled = {
595						url: marker_map_icon,
596						scaledSize: scaledSize,
597					};
598					var marker_hover_icon_scaled = {
599						url: marker_map_icon_hover,
600						scaledSize: scaledSize,
601					};
602
603					self.api.event.addListener( self.markers[ map.map ][ marker.marker ], 'mouseover', function() {
604						self.markers[ map.map ][ marker.marker ].setIcon( marker_hover_icon_scaled );
605					});
606					self.api.event.addListener( self.markers[ map.map ][ marker.marker ], 'mouseout', function() {
607						self.markers[ map.map ][ marker.marker ].setIcon( marker_icon_scaled );
608					});
609					// Add custom classnames to reproduce this hover effect from an HTML element
610					$( document ).on( 'mouseover', '.js-toolset-maps-hover-map-' + map.map + '-marker-' + marker.marker, function() {
611						self.markers[ map.map ][ marker.marker ].setIcon( marker_hover_icon_scaled );
612					});
613					$( document ).on( 'mouseout', '.js-toolset-maps-hover-map-' + map.map + '-marker-' + marker.marker, function() {
614						self.markers[ map.map ][ marker.marker ].setIcon( marker_icon_scaled );
615					});
616				}
617			}
618
619			if ( marker.markerinfowindow !== '' ) {
620				// Create a single self.api.InfoWindow object for each map, if needed, and populate its content based on
621				// the marker
622				self.infowindows[ map.map ] = self.infowindows[ map.map ] || new self.api.InfoWindow({ content: '' });
623				self.api.event.addListener( self.markers[ map.map ][ marker.marker ], clickEvent, function() {
624					self.infowindows[ map.map ].setContent( marker.markerinfowindow );
625					self.infowindows[ map.map ].open( self.maps[ map.map ], self.markers[ map.map ][ marker.marker ] );
626				});
627				$( document ).on(
628					'click',
629					'.js-toolset-maps-open-infowindow-map-' + map.map + '-marker-' + marker.marker,
630					function() {
631						self.infowindows[ map.map ].setContent( marker.markerinfowindow );
632						self.openInfowindowWhenMarkerVisible( map.map, marker.marker );
633					}
634				);
635			}
636
637			if ( spiderfy ) {
638				oms.addMarker( self.markers[ map.map ][ marker.marker ] )
639			}
640		});
641
642		if ( _.size( map.markers ) == 1 ) {
643			if ( map.options['single_zoom'] != '' ) {
644				self.maps[ map.map ].setZoom( map.options['single_zoom'] );
645				if ( map.options['fitbounds'] == 'on' ) {
646					self.api.event.addListenerOnce( self.maps[ map.map ], 'bounds_changed', function( event ) {
647						self.maps[ map.map ].setZoom( map.options['single_zoom'] );
648					});
649				}
650			}
651			if ( map.options['single_center'] == 'on' ) {
652				for ( var mark in self.markers[ map.map ] ) {
653					self.maps[ map.map ].setCenter( self.markers[ map.map ][ mark ].getPosition() );
654					break;
655				}
656			}
657		} else if ( _.size( map.markers ) > 1 ) {
658			if ( map.options['fitbounds'] == 'on' ) {
659				self.maps[ map.map ].fitBounds( self.bounds[ map.map ] );
660			}
661		}
662
663		if ( _.contains( self.resize_queue, map.map ) ) {
664			self.keep_map_center_and_resize( self.maps[ map.map ] );
665			_.reject( self.resize_queue, function( item ) {
666				return item == map.map;
667			});
668		}
669
670		$( document ).trigger( 'js_event_wpv_addon_maps_init_map_completed', [ event_settings ] );
671
672		// Init Street View overlay if lat and long are provided (coming from address)
673		if ( map.options.lat && map.options.long ) {
674			self.initStreetView( event_settings, new google.maps.LatLng( map.options.lat, map.options.long ) );
675		}
676	};
677	/**
678	 * Make sure that marker is visible (not in cluster) before opening info 
678window. If it isn't, wait until it is.
679	 *
680	 * Because, if marker isn't visible, info window will get wrong location to open (usually another, visible marker).
681	 *
682	 * @param {String} mapId
683	 * @param {String} markerId
684	 * @since 1.5
685	 */
686	self.openInfowindowWhenMarkerVisible = function( mapId, markerId ) {
687		if ( self.markers[mapId][markerId].map ) {
688			self.infowindows[mapId].open( self.maps[mapId], self.markers[mapId][markerId] );
689		} else {
690			_.delay(function () {
691				self.openInfowindowWhenMarkerVisible( mapId, markerId );
692			}, 150);
693		}
694	};
695	/**
696	* clean_map_data
697	*
698	* @param map_id
699	*
700	* @since 1.0
701	*/
702
703	self.clean_map_data = function( map_id ) {
704		self.maps_data = _.filter( self.maps_data, function( map_data_unique ) {
705			return map_data_unique.map != map_id;
706		});
707
708		self.maps				= _.omit( self.maps, map_id );
709		self.markers			= _.omit( self.markers, map_id );
710		self.infowindows		= _.omit( self.infowindows, map_id );
711		self.bounds				= _.omit( self.bounds, map_id );
712		self.cluster_options	= _.omit( self.cluster_options, map_id );
713
714		var settings = {
715			map_id: map_id
716		};
717
718		$( document ).trigger( 'js_event_wpv_addon_maps_clean_map_completed', [ settings ] );
719
720	};
721
722	/**
723	* keep_map_center_and_resize
724	*
725	* @param map
726	*
727	* @since 1.1
728	*/
729
730	self.keep_map_center_and_resize = function( map ) {
731		var map_iter_center = map.getCenter();
732		self.api.event.trigger( map, "resize" );
733		map.setCenter( map_iter_center );
734	};
735
736	/**
737	 * Init all maps - Azure API version
738	 * @since 1.5
739	 */
740	self.initMapsAzure = function() {
741		let maps = _.map( self.maps_data, self.resolveGeolocatedMarkerThenInitMapAzure );
742		let mapIds = _.pluck( self.maps_data, 'map' );
743
744		self.maps = _.object( mapIds, maps );
745	};
746
747	/**
748	 * Resolve geolocated marker (if any)
749	 *
750	 * Among markers for a map, there may be a special one which should trigger current visitor geolocation. (It makes
751	 * no sense to have more than one, because the visitor is not a quark). This method checks if there is one such
752	 * marker, if geolocation is available, and if the map rendering should wait for geolocation or if that marker
753	 * should be added after map and other markers render.
754	 *
755	 * @since 1.5
756	 *
757	 * @param {Object} map
758	 */
759	self.resolveGeolocatedMarkerThenInitMapAzure = function( map ) {
760		let geoMarker = _.findWhere( map.markers, {markerlat: 'geo'} );
761		let renderedMap = null;
762
763		if ( geoMarker ) {
764			let geoMarkerIndex = _.indexOf( map.markers, geoMarker );
765
766			if ( "immediate" === geoMarker.markerlon ) {
767				// Render map immediately, marker will be added when and if available, center and bounds adjusted if
768				// needed.
769				renderedMap = self.initMapAzure( map );
770
771				if (navigator.geolocation) {
772					navigator.geolocation.getCurrentPosition(function (position) {
773						map.markers[geoMarkerIndex].markerlat = position.coords.latitude;
774						map.markers[geoMarkerIndex].markerlon = position.coords.longitude;
775
776						if (
777							map.markers.length === 1
778							&& map.options.single_center === "on"
779						) {
780							let mapCenter = [position.coords.longitude, position.coords.latitude];
781							renderedMap.setCamera( {center: mapCenter} );
782						}
783
784						self.maybeFitboundsAzure( map, renderedMap );
785
786						let pin = new atlas.data.Feature(
787							new atlas.data.Point( [position.coords.longitude, position.coords.latitude] ),
788							{title: geoMarker.title, popup: geoMarker.markerinfowindow},
789							geoMarker.marker
790						);
791						renderedMap.addEventListener('load', function() {
792							renderedMap.addPins( [pin], {
793								fontColor: "#000",
794								fontSize: 14,
795								icon: "pin-red",
796								iconSize: 1,
797								name: "default-pin-layer",
798								cluster: ( map.options.cluster === 'on' ),
799								textOffset: [0, 20],
800							});
801						} );
802					});
803				}
804			} else {
805				if (navigator.geolocation) {
806					navigator.geolocation.getCurrentPosition(function (position) {
807						map.markers[geoMarkerIndex].markerlat = position.coords.latitude;
808						map.markers[geoMarkerIndex].markerlon = position.coords.longitude;
809
810						// We now have position for geo marker, render as usual.
811						renderedMap = self.initMapAzure( map );
812					});
813				} else {
814					// No geolocation? Render what we have, without this marker.
815					renderedMap = self.initMapAzure( map );
816				}
817			}
818		} else {
819			// No geomarker, just render as usual.
820			renderedMap = self.initMapAzure( map );
821		}
822
823		return renderedMap;
824	};
825
826	/**
827	 * If there is more than 1 marker and fitbounds is requested for this map, find bound points and set camera bounds.
828	 *
829	 * @since 1.5
830	 *
831	 * @param {Object} map
832	 * @param {atlas.Map} renderedMap
833	 */
834	self.maybeFitboundsAzure = function( map, renderedMap ) {
835		if ( map.markers.length <= 1 || map.options.fitbounds !== "on" ) return;
836
837		let markerLons = _.pluck( map.markers, 'markerlon' );
838		let southernmost = _.min( markerLons );
839		let northernmost = _.max( markerLons );
840		let markerLats = _.pluck( map.markers, 'markerlat' );
841		let westmost = _.min( markerLats );
842		let eastmost = _.max( markerLats );
843
844		renderedMap.setCameraBounds( {
845			bounds: [southernmost, westmost, northernmost, eastmost],
846			padding: 50
847		} );
848	};
849
850	/**
851	 * Map type names are sometimes, but not always the same between Google and Azure.
852	 *
853	 * @since 1.5.3
854	 * @param {string} mapType
855	 * @return {string}
856	 */
857	self.translateMapTypesToAzure = function( mapType ) {
858		const translationTable = {
859			'roadmap': 'road',
860			'hybrid': 'satellite_road_labels',
861			'terrain': 'road' // 'terrain' type is not supported by Azure, so render the closest - 'road'
862		};
863
864		if ( mapType in translationTable ) return translationTable[mapType];
865		// 'satellite' type has the same name, so just falls through.
866		else return mapType;
867	};
868
869	/**
870	 * Returns value for marker icon set from marker, map, or default
871	 *
872	 * @since 1.5.3
873	 * @param {Object} marker
874	 * @param {Object} mapOptions
875	 * @return {string}
876	 */
877	self.getMarkerIconValue = function( marker, mapOptions ) {
878		if ( marker.markericon ) {
879			return marker.markericon;
880		} else if ( mapOptions.marker_icon ) {
881			return mapOptions.marker_icon;
882		} else {
883			return 'pin-red';
884		}
885	};
886
887	/**
888	 * Init a map with Azure API
889	 *
890	 * @since 1.5
891	 *
892	 * @param {Object} map
893	 *
894	 * @return {atlas.Map}
895	 */
896	self.initMapAzure = function( map ) {
897		let event_settings = {
898			map_id:			map.map,
899			map_options:	map.options
900		};
901		$( document ).trigger( 'js_event_wpv_addon_maps_init_map_started', [ event_settings ] );
902
903		// Init defaults from map options
904		let mapCenter = [map.options.general_center_lon, map.options.general_center_lat];
905		let mapZoom = map.options.general_zoom;
906		let cluster = ( map.options.cluster === 'on' );
907
908		// 1 marker logic
909		if ( map.markers.length === 1 ) {
910			if (
911				// marker geolocated and rendered after map, handled in self.resolveGeolocatedMarkerThenInitMapAzure
912				_.isNumber( map.markers[0].markerlon )
913				&& map.options.single_center === "on"
914			) {
915				mapCenter = [map.markers[0].markerlon, map.markers[0].markerlat];
916			}
917
918			mapZoom = map.options.single_zoom;
919		}
920
921		// Remove possible map loading content. Google Maps does it by itself, Azure doesn't
922		$( '#js-wpv-addon-maps-render-' + map.map ).empty();
923
924		// Render map
925		let renderedMap = new atlas.Map( "js-wpv-addon-maps-render-" + map.map, {
926			"subscription-key": views_addon_maps_i10n.azure_api_key,
927			center: mapCenter,
928			zoom: mapZoom,
929			scrollZoomInteraction: ( map.options.scrollwheel === 'on' ),
930			dblClickZoomInteraction: ( map.options.double_click_zoom === 'on' ),
931			dragPanInteraction: ( map.options.draggable === 'on' ),
932			style: self.translateMapTypesToAzure( map.options.map_type ) // Ignored with API 1.1, but works with 1.2
933		} );
934
935		$( document ).trigger( 'js_event_wpv_addon_maps_init_map_inited', [ event_settings ] );
936
937		// Bounds can only be set on an already rendered map with Azure API
938		self.maybeFitboundsAzure( map, renderedMap );
939
940		// Create pins (markers)
941		let pins = _.map( map.markers, function( marker ) {
942			return new atlas.data.Feature(
943				new atlas.data.Point( [marker.markerlon, marker.markerlat] ),
944				{
945					title: marker.title,
946					popup: marker.markerinfowindow,
947					icon: self.getMarkerIconValue( marker, map.options ),
948					iconhover: marker.markericonhover ? marker.markericonhover : map.options.marker_icon_hover,
949					id: marker.marker
950				},
951				marker.marker
952			);
953		} );
954
955		// Create custom icons for pins (if any)
956		let icons = _.filter(
957			_.union(
958				_.pluck( map.markers, 'markericon' ),
959				_.pluck( map.markers, 'markericonhover' ),
960				[map.options.marker_icon, map.options.marker_icon_hover]
961			)
962		);
963
964		// Add markers and their events to map (waiting for load event first, otherwise the library throws a style
965		// rendering warning)
966		renderedMap.addEventListener('load', function() {
967			_.each( icons, function( icon ) {
968				var iconImg = document.getElementById( icon );
969
970				if ( iconImg ) {
971					renderedMap.addIcon( icon, iconImg );
972				}
973			} );
974
975			self.addPins( renderedMap, pins, cluster );
976
977			// FUTURE: this needs Azure Maps API 1.2, which is buggy at the moment and kills some other things. Revisit.
978			// Add controls
979			/*if ( map.options.zoom_control === 'on' ) {
980				renderedMap.addControl(new atlas.control.ZoomControl, {});
981			}
982			if ( map.options.map_type_control === 'on' ) {
983				renderedMap.addControl(new atlas.control.StyleControl, {});
984			}*/
985
986			// Add an event listener for click (popup)
987			renderedMap.addEventListener( 'click', "default-pin-layer", function( event ) {
988				// Does pin have some popup text?
989				if ( ! event.features[0].properties.popup ) return;
990
991				// Create content for popup
992				let popupContentElement = document.createElement("div");
993				popupContentElement.style.padding = "8px";
994				let popupNameElement = document.createElement("div");
995				popupNameElement.innerHTML = event.features[0].properties.popup;
996				popupContentElement.appendChild(popupNameElement);
997
998				// Create a popup
999				let popup = new atlas.Popup( {
1000					content: popupContentElement,
1001					position: event.features[0].geometry.coordinates,
1002					pixelOffset: [0, 0]
1003				} );
1004
1005				popup.open( renderedMap );
1006			} );
1007
1008			// Add event listeners for hover (possible marker icon change)
1009			let originalIcon = '';
1010			let hoveredPinIndex = 0;
1011			renderedMap.addEventListener( 'mouseover', "default-pin-layer", function( event ) {
1012				if ( ! event.features[0].properties.iconhover ) return;
1013
1014				let id = event.features[0].properties.id;
1015				let pin = _.findWhere( pins, {id: id} );
1016
1017				originalIcon = event.features[0].properties.icon;
1018				hoveredPinIndex = _.indexOf( pins, pin );
1019
1020				pin.properties.icon = pin.properties.iconhover;
1021				pins[hoveredPinIndex] = pin;
1022
1023				self.addPins( renderedMap, pins, cluster );
1024			} );
1025			renderedMap.addEventListener( 'mouseout', "default-pin-layer", function( event ) {
1026				if ( ! originalIcon ) return;
1027
1028				pins[hoveredPinIndex].properties.icon = originalIcon;
1029
1030				self.addPins( renderedMap, pins, cluster );
1031
1032				originalIcon = '';
1033			} );
1034		} );
1035
1036		$( document ).trigger( 'js_event_wpv_addon_maps_init_map_completed', [ event_settings ] );
1037
1038		return renderedMap;
1039	};
1040
1041	/**
1042	 * Adds (or changes) all the pins for the given map on default pin layer.
1043	 *
1044	 * (Because you cannot simply change one pin with Azure, you have to change the layer).
1045	 *
1046	 * @since 1.5.3
1047	 * @param atlas.Map map
1048	 * @param {array} pins
1049	 * @param {bool} cluster
1050	 */
1051	self.addPins = function( map, pins, cluster ) {
1052		map.addPins( pins, {
1053			fontColor: "#000",
1054			fontSize: 14,
1055			name: "default-pin-layer",
1056			cluster: cluster,
1057			textOffset: [0, 20],
1058			overwrite: true
1059		} );
1060	};
1061
1062	// ------------------------------------
1063	// API
1064	// ------------------------------------
1065
1066	/**
1067	* WPViews.view_addon_maps.reload_map
1068	*
1069	* @param map_id
1070	*
1071	* @since 1.0
1072	*/
1073
1074	self.reload_map = function( map_id ) {
1075		var settings = {
1076			map_id: map_id
1077		};
1078		$( document ).trigger( 'js_event_wpv_addon_maps_reload_map_started', [ settings ] );
1079		$( document ).trigger( 'js_event_wpv_addon_maps_reload_map_triggered', [ settings ] );
1080		$( document ).trigger( 'js_event_wpv_addon_maps_reload_map_completed', [ settings ] );
1081	};
1082
1083	/**
1084	* document.js_event_wpv_addon_maps_reload_map_triggered
1085	*
1086	* @param event
1087	* @param data
1088	* 	data.map_id
1089	*
1090	* @since 1.0
1091	* @since 1.5 supports Azure API
1092	*/
1093	$( document ).on( 'js_event_wpv_addon_maps_reload_map_triggered', function( event, data ) {
1094		var defaults = {
1095			map_id: false
1096		},
1097		settings = $.extend( {}, defaults, data );
1098		if (
1099			settings.map_id == false
1100			|| $( '#js-wpv-addon-maps-render-' + settings.map_id ).length != 1
1101		) {
1102			return;
1103		}
1104		self.clean_map_data( settings.map_id );
1105		var mpdata = self.collect_map_data( $( '#js-wpv-addon-maps-render-' + settings.map_id ) );
1106
1107		if ( API_GOOGLE === views_addon_maps_i10n.api_used ) {
1108			self.init_map_after_loading_styles( mpdata );
1109		} else {
1110			$( '#js-wpv-addon-maps-render-' + settings.map_id ).empty();
1111			self.maps[settings.map_id] = self.resolveGeolocatedMarkerThenInitMapAzure( mpdata );
1112		}
1113	});
1114
1115	/**
1116	* WPViews.view_addon_maps.get_map
1117	*
1118	* @param map_id
1119	*
1120	* @return google.maps.Map object | false
1121	*
1122	* @since 1.0
1123	*/
1124
1125	self.get_map = function( map_id ) {
1126		if ( map_id in self.maps ) {
1127			return self.maps[ map_id ];
1128		} else {
1129			return false;
1130		}
1131	};
1132
1133	/**
1134	* WPViews.view_addon_maps.get_map_marker
1135	*
1136	* @param marker_id
1137	* @param map_id
1138	*
1139	* @return google.maps.Marker object | false
1140	*
1141	* @since 1.0
1142	*/
1143
1144	self.get_map_marker = function( marker_id, map_id ) {
1145		if (
1146			map_id in self.markers
1147			&& marker_id in self.markers[ map_id ]
1148		) {
1149			return self.markers[ map_id ][ marker_id ];
1150		} else {
1151			return false;
1152		}
1153	};
1154
1155	// ------------------------------------
1156	// Interaction
1157	// ------------------------------------
1158
1159	/**
1160	* Reload on js-wpv-addon-maps-reload-map.click
1161	*
1162	* @since 1.0
1163	*/
1164
1165	$( document ).on( 'click', '.js-wpv-addon-maps-reload-map', function( e ) {
1166		e.preventDefault();
1167		var thiz = $( this );
1168		if ( thiz.attr( 'data-map' ) ) {
1169			self.reload_map( thiz.data( 'map' ) );
1170		}
1171	});
1172
1173	/**
1174	* Center on a marker on js-wpv-addon-maps-center-map.click
1175	*
1176	* @since 1.0
1177	* @since 1.5 Azure API supported
1178	*/
1179	$( document ).on( 'click', '.js-wpv-addon-maps-focus-map', function( e ) {
1180		e.preventDefault();
1181		var thiz = $( this ),
1182		thiz_map,
1183		thiz_marker,
1184		thiz_zoom;
1185		if (
1186			thiz.attr( 'data-map' )
1187			&& thiz.attr( 'data-marker' )
1188		) {
1189			thiz_map = thiz.data( 'map' );
1190			thiz_marker = thiz.data( 'marker' );
1191
1192			if ( API_GOOGLE === views_addon_maps_i10n.api_used ) {
1193				if (
1194					thiz_map in self.maps
1195					&& thiz_map in self.markers
1196					&& thiz_marker in self.markers[thiz_map]
1197				) {
1198					thiz_zoom = ($('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom') != '')
1199						? $('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom')
1200						: 14;
1201					self.maps[thiz_map].setCenter(self.markers[thiz_map][thiz_marker].getPosition());
1202					self.maps[thiz_map].setZoom(thiz_zoom);
1203				}
1204			} else {
1205				if (
1206					thiz_map in self.maps
1207				) {
1208					thiz_zoom = ($('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom') != '')
1209						? $('#js-wpv-addon-maps-render-' + thiz_map).data('singlezoom')
1210						: 14;
1211					var $thisMarker = $('.js-wpv-addon-maps-marker-' + thiz_marker);
1212
1213					self.maps[thiz_map].setCamera({
1214						center: [$thisMarker.eq(0).data('markerlon'), $thisMarker.eq(0).data('markerlat')],
1215						zoom: thiz_zoom
1216					});
1217				}
1218			}
1219		}
1220	});
1221
1222	/**
1223	* Center map on fitbounds on js-wpv-addon-maps-center-map-fitbounds.click
1224	*
1225	* @todo make the restore work when having several points and no fitbounds, or a single point and no single_zoom or single_center
1226	*
1227	* @since 1.0
1228	*/
1229
1230	$( document ).on( 'click', '.js-wpv-addon-maps-restore-map', function( e ) {
1231		e.preventDefault();
1232		var thiz = $( this ),
1233		thiz_map,
1234		current_map_data_array,
1235		current_map_data;
1236		if ( thiz.attr( 'data-map' ) ) {
1237			thiz_map = thiz.data( 'map' );
1238			if (
1239				thiz_map in self.maps
1240				&& thiz_map in self.bounds
1241			) {
1242				self.maps[ thiz_map ].fitBounds( self.bounds[ thiz_map ] );
1243			}
1244
1245			if ( _.size( self.markers[ thiz_map ] ) == 1 ) {
1246				current_map_data_array = _.filter( self.maps_data, function( map_data_unique ) {
1247					return map_data_unique.map == thiz_map;
1248				});
1249				current_map_data = _.first( current_map_data_array );
1250				if ( current_map_data.options['single_zoom'] != '' ) {
1251					self.maps[ thiz_map ].setZoom( current_map_data.options['single_zoom'] );
1252					if ( current_map_data.options['fitbounds'] == 'on' ) {
1253						self.api.event.addListenerOnce( self.maps[ thiz_map ], 'bounds_changed', function( event ) {
1254							self.maps[ thiz_map ].setZoom( current_map_data.options['single_zoom'] );
1255						});
1256					}
1257				}
1258				if ( current_map_data.options['single_center'] == 'on' ) {
1259					for ( var mark in self.markers[ thiz_map ] ) {
1260						self.maps[ thiz_map ].setCenter( self.markers[ thiz_map ][ mark ].getPosition() );
1261						break;
1262					}
1263				}
1264			}
1265
1266			if ( API_AZURE === views_addon_maps_i10n.api_used ) {
1267				current_map_data_array = _.filter( self.maps_data, function( map_data_unique ) {
1268					return map_data_unique.map == thiz_map;
1269				});
1270				current_map_data = _.first( current_map_data_array );
1271
1272				if ( _.size( current_map_data.markers ) === 1 ) {
1273					var $thisMarker = $('.js-wpv-addon-maps-markerfor-' + thiz_map);
1274					self.maps[thiz_map].setCamera({
1275						center: [$thisMarker.eq(0).data('markerlon'), $thisMarker.eq(0).data('markerlat')],
1276						zoom: current_map_data.options.single_zoom
1277					});
1278				} else {
1279					self.maybeFitboundsAzure(current_map_data, self.maps[thiz_map]);
1280				}
1281			}
1282		}
1283	});
1284
1285	// ------------------------------------
1286	// Views compatibility
1287	// ------------------------------------
1288
1289	/**
1290	* Reload the maps contained on the parametric search results loaded via AJAX
1291	*
1292	* @since 1.0
1293	*/
1294	$( document ).on( 'js_event_wpv_parametric_search_results_updated', function( event, data ) {
1295		self.cleanupMarkersFromMapBlock( data.view_unique_id );
1296		_.each( self.get_affected_maps( data.layout ), self.reload_map );
1297	} );
1298
1299	/**
1300	* Reload the maps contained on the pagination results loaded via AJAX
1301	*
1302	* @since 1.0
1303	*/
1304	$( document ).on( 'js_event_wpv_pagination_completed', function( event, data ) {
1305		self.cleanupMarkersFromMapBlock( data.view_unique_id );
1306		_.each( self.get_affected_maps( data.layout ), self.reload_map );
1307	} );
1308
1309	/**
1310	 * Check if a Map block contains markers coming from a View, and it it does, remove those, because new ones are
1311	 * provided by View.
1312	 *
1313	 * @since 2.0
1314	 * @param {int|string} viewUniqueId
1315	 */
1316	self.cleanupMarkersFromMapBlock = function( viewUniqueId ) {
1317		const viewId = _.isNumber( viewUniqueId ) ? viewUniqueId : viewUniqueId.split( '-' )[ 0 ];
1318
1319		$( '.js-wpv-addon-maps-marker' ).each( function( key, marker ) {
1320			// If this is a marker coming from given View...
1321			if ( Number( $( marker ).data( 'fromview' ) ) === Number( viewId ) ) {
1322				// Record View - Map blocks relation, as it's useful in some situations
1323				self.view_map_blocks_relation[ viewUniqueId ] = $( marker ).data( 'markerfor' );
1324
1325				// Remove markers next to Map div (instead of in View div)
1326				$( marker ).prev( '.js-wpv-addon-maps-render' ).next().remove();
1327			}
1328		} );
1329	};
1330
1331	/**
1332	* get_affected_maps
1333	*
1334	* Get all the maps that have related data used in the given containr, no matter if a map render or a marker
1335	*
1336	* @param container	object
1337	*
1338	* @return array
1339	*
1340	* @since 1.1
1341	*/
1342
1343	self.get_affected_maps = function( container ) {
1344		var affected_maps = [];
1345		container.find( '.js-wpv-addon-maps-render' ).each( function() {
1346			affected_maps.push( $( this ).data( 'map' ) );
1347		});
1348		container.find( '.js-wpv-addon-maps-marker' ).each( function() {
1349			affected_maps.push( $( this ).data( 'markerfor' ) );
1350		});
1351		if ( self.view_map_blocks_relation[ container.data( 'viewnumber' ) ] ) {
1352			affected_maps.push( self.view_map_blocks_relation[ container.data( 'viewnumber' ) ] );
1353		}
1354		affected_maps = _.uniq( affected_maps );
1355		return affected_maps;
1356	};
1357
1358	// ------------------------------------
1359	// Clusters definitions and interactions
1360	// ------------------------------------
1361
1362	/**
1363	* WPViews.view_addon_maps.set_cluster_options
1364	*
1365	* Sets options for clusters, either global or for a specific map
1366	*
1367	* @param options	object
1368	*		@param options.styles = [
1369	* 			{
1370	* 				url:		string		URL of the cluster image for this style,
1371	* 				height:		integer		Width of the cluster image for this style,
1372	* 				width:		integer		Height of the cluster image for this style,
1373	* 				textColor:	string		(optional) Color of the counter text in this cluster, as a color name or hex value (with #),
1374	* 				textSize	integer		(optional) Text size for the counter in this cluster, in px (without unit)
1375	* 			},
1376	* 			{ ... }
1377	* 		]
1378	* 		@param options.calculator = function( markers, numStyles ) {
1379	* 			@param markers		array	Markers in this cluster
1380	* 			@param numStyles	integer	Number of styles defined
1381	* 			@return {
1382	* 				text:	string		Text to be displayed inside the marker,
1383	* 				index:	integer		Index of the options.styles array that will be applied to this cluster - please make it less than numStyles
1384	* 			};
1385	* 		}
1386	* @param map_id		(optional) string The map ID this options will be binded to, global otherwise
1387	*
1388	* @note Most of the cluster options for a map are set in the map shortcode
1389	* @note Maps without specific styling options will get the current global options
1390	* @note We stoe the options in self.has_cluster_options for later usage, like on reload events
1391	*
1392	* @since 1.0
1393	*/
1394
1395	self.set_cluster_options = function( options, map_id ) {
1396		if ( typeof map_id === 'undefined' ) {
1397			// If map_id is undefined, set global options
1398			if ( _.has( options , "styles" ) ) {
1399				self.default_cluster_options['styles'] = options['styles'];
1400			}
1401			if ( _.has( options , "calculator" ) ) {
1402				self.default_cluster_options['calculator'] = options['calculator'];
1403			}
1404		} else {
1405			// Otherwise, bind to a specific map ID
1406			// Note that defaults are also used
1407			self.cluster_options[ map_id ] = self.get_cluster_options( map_id );
1408			if ( _.has( options , "styles" ) ) {
1409				self.cluster_options[ map_id ]['styles'] = options['styles'];
1410			}
1411			if ( _.has( options , "calculator" ) ) {
1412				self.cluster_options[ map_id ]['calculator'] = options['calculator'];
1413			}
1414			self.has_cluster_options[ map_id ] = options;
1415		}
1416	};
1417
1418	/**
1419	* WPViews.view_addon_maps.get_cluster_options
1420	*
1421	* Gets options for clusters, either global of for a specific map
1422	*
1423	* @param map_id		(optional) string	The map ID to get options from
1424	*
1425	* @return options	object				Set of options, either global or dedicated if the passed map_id has specific options
1426	*
1427	* @since 1.0
1428	*/
1429
1430	self.get_cluster_options = function( map_id ) {
1431		var options = self.default_cluster_options;
1432		if (
1433			typeof map_id !== 'undefined'
1434			&& _.has( self.cluster_options, map_id )
1435		) {
1436			options = self.cluster_options[ map_id ];
1437		}
1438		return options;
1439	};
1440
1441	// ------------------------------------
1442	// Init
1443	// ------------------------------------
1444
1445	/**
1446	 * Inits Google Maps API specific code paths
1447	 * @since 1.5
1448	 */
1449	self.initGoogle = function() {
1450		if ( self.api === null ) {
1451			if ( self.maps_data.length ) {
1452				self.maybeLazyLoadGoogle();
1453			}
1454			return;
1455		} else if ( self.api === false ) { // Lazy loading failed, don't get into an infinite loop.
1456			return;
1457		}
1458
1459		self.api.event.addDomListener( window, "resize", function() {
1460			_.each( self.maps, function( map_iter, map_id ) {
1461				self.keep_map_center_and_resize( map_iter );
1462			});
1463		});
1464
1465		$( document ).on( 'js_event_wpv_layout_responsive_resize_completed', function( event ) {
1466			$( '.js-wpv-layout-responsive .js-wpv-addon-maps-render' ).each( function() {
1467				var thiz = $( this ),
1468					thiz_map = thiz.data( 'map' );
1469				if ( thiz_map in self.maps ) {
1470					self.keep_map_center_and_resize( self.maps[ thiz_map ] );
1471				} else {
1472					self.resize_queue.push( thiz_map );
1473					_.uniq( self.resize_queue );
1474				}
1475			});
1476		});
1477
1478		self.maybeInitElementorEditorPreviewFix( self.init_maps );
1479
1480		self.init_maps();
1481	};
1482
1483	/**
1484	 * Inits Microsoft Azure Maps specific code paths
1485	 * @since 1.5
1486	 */
1487	self.initAzure = function() {
1488		self.initMapsAzure();
1489		self.maybeInitElementorEditorPreviewFix( self.initMapsAzure );
1490	};
1491
1492	/**
1493	 * Initialize maps on Elementor widget ready so it works when previewing an Elementor design.
1494	 *
1495	 * @since 1.5.3
1496	 *
1497	 * @param {Function} mapInitCallback
1498	 */
1499	self.maybeInitElementorEditorPreviewFix = function( mapInitCallback ) {
1500		if (typeof elementor === 'object' && typeof elementorFrontend === 'object') {
1501			elementorFrontend.hooks.addAction('frontend/element_ready/widget', function ($scope) {
1502				var mapInScope = $scope.find('div.js-wpv-addon-maps-render');
1503				if (mapInScope.length) {
1504					self.collect_maps_data();
1505					mapInitCallback();
1506				}
1507			});
1508		}
1509	};
1510
1511	/**
1512	 * Initializes a single map by given id, and also cleans up if map with same id was already initialized.
1513	 *
1514	 * API agnostic.
1515	 *
1516	 * @since 1.7
1517	 * @param {string} id
1518	 */
1519	self.initMapById = function( id ) {
1520		self.clean_map_data( id );
1521
1522		const mapData = self.collect_map_data( $( 'div#js-wpv-addon-maps-render-' + id ) );
1523
1524		// It might happen that this method was called, but the actual map wasn't rendered yet. In that case, it will
1525		// delay itself until the map HTML is ready.
1526		if ( ! mapData.map ) {
1527			_.delay( self.initMapById, 200, id );
1528			return;
1529		}
1530
1531		if ( API_GOOGLE === views_addon_maps_i10n.api_used ) {
1532			self.maybeLazyLoadGoogle( self.init_map_after_loading_styles, mapData );
1533		} else {
1534			self.maps[id] = self.resolveGeolocatedMarkerThenInitMapAzure( mapData );
1535		}
1536	};
1537
1538	/**
1539	 * Lazy load Google libraries and other libraries depending on it, if needed.
1540	 * @param {Function} initMap
1541	 * @param {object} mapData Callback parameter.
1542	 */
1543	self.maybeLazyLoadGoogle = function( initMap = null, mapData = null ) {
1544		if ( self.api === null ) {
1545			$.getScript(
1546				`https://maps.googleapis.com/maps/api/js?libraries=places&v=3&key=${ views_addon_maps_i10n.google_api_key }&callback=Function.prototype`
1547			)
1548				.done( function() {
1549					self.api = window.google.maps;
1550					self.initGoogle(); // Do full init, because only now we have the API ready.
1551				} )
1552				.fail( function() {
1553					self.api = false;
1554					console.warn( 'Toolset Maps: failed to lazy load Google Maps library.' );
1555				} );
1556		} else {
1557			initMap( mapData );
1558			// This is emitted only by collect_maps_data, not by the single map data collector that's used here, and is
1559			// needed by add_current_visitor_location_after_geolocation in order to run at the right time. (Google API
1560			// only).
1561			$( document ).trigger('js_event_wpv_addon_maps_map_data_collected');
1562		}
1563	}
1564
1565	self.init = function() {
1566		self.collect_maps_data();
1567
1568		if ( API_GOOGLE === views_addon_maps_i10n.api_used ) {
1569			self.initGoogle();
1570		} else {
1571			self.initAzure();
1572		}
1573	};
1574
1575	self.init();
1576};
1577
1578jQuery( function( $ ) {
1579	WPViews.view_addon_maps = new WPViews.ViewAddonMaps( $ );
1580});

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.