PageSourceSearch

https://www.quebec-cite.com/themes/custom/base_otq/js/modules/Map.js?tlxdjk

js quebec-cite.com collected 2026-10-02 04:04:34 UTC 31,538 bytes, 1,040 lines download raw bytes

1var OtqMap = function ($, Drupal, Bootstrap) {
2	let self = {};
3	let mapScriptLoaded = false;
4
5	self.mapSlider = null;
6	self.$cartographyMaps = [];
7
8	let $overlay, $mapMarkerAjax;
9	let stickyMapInitialized = false;
10	let zonesCleared = false;
11	let pageClickDelay;
12
13	/** -----------------------------
14   * Routes
15   --------------------------------- */
16
17	/**
18	 * Init default map behavior, only btn behavior is tracked
19	 */
20	self.init = function (e) {
21		$overlay = $('.map-overlay');
22		$mapMarkerAjax = $('#map-marker-ajax');
23		let $pagination = $overlay.find('.pagination');
24
25		CossetteApp.$body.on('click', '.btn-map', onMapBtnClick);
26
27		$overlay.on('click', '.btn-map-controls-resize', onMapResize);
28		$overlay.on('click', '.btn-map-controls-geolocate', onLocationClick);
29		$overlay.on('click', '.marker-trigger', onMarkerTriggerClick);
30		$overlay.on('mouseenter', '.marker-trigger', onMarkerEnter);
31		$overlay.on('mouseleave', '.marker-trigger', onMarkerLeave);
32		$overlay.on('click', '.explore-nav button', onExploreNavClick);
33		$overlay.on('click', '.btn-close-neighbourhood', onCloseNeighbourhood);
34		$pagination.on('click', 'button', onPaginationClick);
35		$mapMarkerAjax.on('click', '.btn-close', onLeaveInfowindow);
36
37		self.clearMarkers = false;
38	};
39
40	/**
41	 * Init sticky map behavior for list and itinerary pages
42	 */
43	self.initStickyMap = function () {
44		let $container = $('.map-sticky');
45		self.$listContentWrapper = $('#list-content-wrapper');
46
47		if ($container.length) {
48			CossetteApp.$stickyOffset = $('#map-sticky-content');
49
50			if (CossetteApp.$stickyOffset.length) {
51				CossetteApp.$stickyMap = $container;
52				CossetteApp.$stickySteps = $('.map-sticky-item');
53				CossetteApp.$stickyStepsWithMarker = $('.map-sticky-item[data-has-marker="1"]');
54
55				self.handleStickyHeightChange();
56			}
57
58			// mutation observer on article height
59			const stickyContentElement = document.getElementById('map-sticky-content');
60			if (stickyContentElement) {
61				const observer = new ResizeObserver(() => {
62					self.handleStickyHeightChange();
63				});
64				observer.observe(stickyContentElement);
65			}
66		}
67
68		if (document.querySelector('.remote-iframe')) {
69			initStickyGmap($container);
70			stickyMapInitialized = true;
71			self.enableListMapMode(0);
72		} else {
73			if (Modernizr.mq('(min-width: 900px)')) {
74				// Use intersection observer to check if map is in viewport before enabling
75				if ('IntersectionObserver' in window) {
76					const lazyMapObserver = new IntersectionObserver(function (entries, observer) {
77						entries.forEach(function (entry) {
78							if (entry.isIntersecting) {
79								initStickyGmap($container);
80								stickyMapInitialized = true;
81								lazyMapObserver.unobserve(entry.target);
82							}
83						});
84					});
85
86					lazyMapObserver.observe($container.get(0));
87				} else {
88					initStickyGmap($container);
89					stickyMapInitialized = true;
90				}
91			}
92		}
93	};
94
95	/** -----------------------------
96   * Public
97   --------------------------------- */
98
99	/**
100	 * Triggered when someones clicks on a map view button in list/itinerary
101	 */
102	self.enableListMapMode = function (startIndex) {
103		if (!stickyMapInitialized) {
104			initStickyGmap($('.map-sticky'));
105			stickyMapInitialized = true;
106		}
107
108		// Ensure $listContentWrapper is initialized
109		if (!self.$listContentWrapper || !self.$listContentWrapper.length) {
110			self.$listContentWrapper = $('#list-content-wrapper');
111		}
112
113		// Ensure $stickyOffset is initialized
114		if (typeof CossetteApp.$stickyOffset === 'undefined' || !CossetteApp.$stickyOffset || !CossetteApp.$stickyOffset.length) {
115			CossetteApp.$stickyOffset = $('#map-sticky-content');
116		}
117
118		// Check if the element exists before using it
119		if (!CossetteApp.$stickyOffset || !CossetteApp.$stickyOffset.length) {
120			console.error('[Map.js] ERROR: #map-sticky-content element not found in DOM');
121			return;
122		}
123
124		// Check if $listContentWrapper exists
125		if (!self.$listContentWrapper || !self.$listContentWrapper.length) {
126			console.error('[Map.js] ERROR: #list-content-wrapper element not found in DOM');
127			return;
128		}
129
130		// Save height to avoid page movement
131		self.$listContentWrapper.height(CossetteApp.$stickyOffset.height());
132
133		let _Slider = CossetteRouter.getClassReference('OtqSlider');
134		_Slider.destroyAll();
135
136		CossetteApp.$html.addClass('has-overlay').removeClass('main-nav-sticky main-nav-showing');
137		self.$listContentWrapper.addClass('map-visible');
138
139		// Bind to slider object
140		let gMapInterval = setInterval(function () {
141			if (typeof CossetteApp.maps[0] !== 'undefined' && CossetteApp.maps[0].loaded === true) {
142				clearInterval(gMapInterval);
143				// Ensure $stickyOffset is still valid before using it
144				if (CossetteApp.$stickyOffset && CossetteApp.$stickyOffset.length) {
145					self.mapSlider = _Slider.initMapSlider(true, CossetteApp.$stickyOffset.get(0), startIndex);
146				}
147			}
148		}, 100);
149	};
150
151	/**
152	 * Triggered when someones clicks on close button of map view in list/itinerary or when reach 900+ width
153	 */
154	self.disableListMapMode = function () {
155		let _Slider = CossetteRouter.getClassReference('OtqSlider');
156
157		CossetteApp.$html.removeClass('has-overlay');
158		self.$listContentWrapper.removeClass('map-visible').css('height', 'auto');
159
160		_Slider.initMapSlider(false, self.mapSlider);
161		_Slider.init();
162
163		// Reset the stick offset to new markup
164		self.mapSlider = null;
165		CossetteApp.$stickyOffset = $('#map-sticky-content');
166		CossetteApp.$stickySteps = $('.map-sticky-item');
167	};
168
169	/**
170	 * Sticky map behavior + active step
171	 * @param currentScroll
172	 */
173	self.adjustMapStickyness = function (currentScroll) {
174		let elementOffsetTop = CossetteApp.stickyOffsetTop;
175		let elementOffsetBottom = CossetteApp.stickyOffsetBottom;
176		let elementOffsetEnd = CossetteApp.stickyOffsetEnd;
177		let elementOffset = self.$listContentWrapper;
178		let elementSteps = CossetteApp.$stickySteps;
179
180		if (currentScroll >= elementOffsetTop) {
181			if (currentScroll <= elementOffsetBottom) {
182				if (!CossetteApp.$stickyMap.hasClass('is-sticky')) {
183					CossetteApp.$stickyMap.css('transform', 'translate3d(0,0,0)');
184					CossetteApp.$stickyMap.addClass('is-sticky').trigger('map-sticky-change');
185				}
186
187				// Don't trigger this when scrollTo has been called
188				if (!CossetteApp.$body.hasClass('is-scrolling')) {
189					setStickyStepState(currentScroll);
190				}
191			} else {
192				if (CossetteApp.$stickyMap.hasClass('is-sticky')) {
193					let posY = elementOffset.outerHeight() - CossetteApp.$window.height();
194					posY += 'px';
195					CossetteApp.$stickyMap.css('transform', 'translate3d(0,' + posY + ',0)');
196					elementSteps.removeClass('is-active');
197					CossetteApp.$stickyMap.removeClass('is-sticky').trigger('map-sticky-change');
198				}
199
200				if (currentScroll <= elementOffsetEnd) {
201					setStickyStepState(currentScroll);
202				} else {
203					setLastStickyStepState();
204				}
205			}
206		} else {
207			if (CossetteApp.$stickyMap.hasClass('is-sticky')) {
208				elementSteps.removeClass('is-active');
209				CossetteApp.$stickyMap.removeClass('is-sticky').trigger('map-sticky-change');
210			}
211		}
212	};
213
214	/**
215	 * Map markers only exist for geolocated list items; convert Splide slide index → markers[] index.
216	 */
217	self.slideIndexToMarkerIndex = function (slideIndex) {
218		if (slideIndex < 0 || typeof slideIndex !== 'number') {
219			return -1;
220		}
221		const slides = document.querySelectorAll('#map-sticky-content .splide__list > .splide__slide:not(.splide__slide--clone)');
222		if (!slides.length || slideIndex >= slides.length) {
223			return -1;
224		}
225		let count = -1;
226		for (let i = 0; i <= slideIndex; i++) {
227			if (slides[i].getAttribute('data-has-marker') === '1') {
228				count++;
229			}
230		}
231		return count;
232	};
233
234	/**
235	 * Reverse: markers[] index (from map click) → Splide slide index.
236	 */
237	self.markerIndexToSlideIndex = function (markerIndex) {
238		if (markerIndex < 0 || typeof markerIndex !== 'number') {
239			return 0;
240		}
241		const slides = document.querySelectorAll('#map-sticky-content .splide__list > .splide__slide:not(.splide__slide--clone)');
242		let seen = -1;
243		for (let i = 0; i < slides.length; i++) {
244			if (slides[i].getAttribute('data-has-marker') === '1') {
245				seen++;
246				if (seen === markerIndex) {
247					return i;
248				}
249			}
250		}
251		return 0;
252	};
253
254	self.updateActiveMapMarkerFromSlider = function (slideIndex) {
255		if (!CossetteApp.maps[0]) {
256			return;
257		}
258		const markerIndex = self.slideIndexToMarkerIndex(slideIndex);
259		if (markerIndex < 0) {
260			return;
261		}
262		let offset = { y: CossetteApp.$stickyMap.height() / 6, x: 0 };
263
264		CossetteApp.maps[0].highlightCurrentMarker(markerIndex);
265
266		if (document.querySelector('article.remote-iframe')) {
267			CossetteApp.maps[0].canvas.setZoom(CossetteApp.maps[0].canvas.getZoom() + 2);
268		}
269
270		CossetteApp.maps[0].handlePan(markerIndex, offset);
271	};
272
273	/**
274	 * When viewport width changes (from Router), recalculate values
275	 */
276	self.handleStickyHeightChange = function () {
277		CossetteApp.stickyOffsetTop = self.$listContentWrapper.offset().top;
278		CossetteApp.stickyOffsetBottom = CossetteApp.stickyOffsetTop + self.$listContentWrapper.outerHeight() - CossetteApp.$window.height();
279		CossetteApp.stickyOffsetEnd = CossetteApp.stickyOffsetBottom + CossetteApp.$window.height();
280
281		CossetteApp.stickyYAdjust = 0;
282		const $listIntro = self.$listContentWrapper.find('.list-intro');
283		const $introWrapper = self.$listContentWrapper.find('.intro-wrapper');
284
285		if ($listIntro.length) {
286			CossetteApp.stickyYAdjust += $listIntro.height();
287		}
288
289		if ($introWrapper.length) {
290			CossetteApp.stickyYAdjust += $introWrapper.height();
291		}
292	};
293
294	/**
295	 * Set offset from context
296	 * @param {int} markerNid : current marker's node ID to select corresponding panel
297	 */
298
299	self.setMapOffset = function (markerNid) {
300		let offset = { y: 0, x: 0 };
301		let $overlayContent = $('.map-overlay .content-container');
302		let mapViewMode = self.checkMapViewMode();
303
304		if (markerNid) {
305			let $markerPanel = $(`.map-marker[data-nid='${markerNid}']`);
306			if (Modernizr.mq('(max-width: 767px)')) {
307				offset = {
308					y: parseInt($markerPanel.height() / 2),
309					x: 0
310				};
311			} else {
312				offset = {
313					y: -60,
314					x: $overlayContent.width() / 2
315				};
316			}
317		} else {
318			if (Modernizr.mq('(max-width: 767px)')) {
319				if (mapViewMode === 'normal') {
320					offset = {
321						y: 130,
322						x: 0
323					};
324				}
325			} else {
326				if (mapViewMode === 'normal' && CossetteApp.$html.hasClass('has-map-overlay')) {
327					offset = {
328						y: 0,
329						x: $overlayContent.width() / 2
330					};
331				}
332			}
333		}
334
335		return offset;
336	};
337
338	/**
339	 * Convert layout offset to padding for fitbounds
340	 * @param offset
341	 * @returns {{left: number, bottom: number}}
342	 */
343	self.convertToPadding = function (offset) {
344		let padding = { left: 30, bottom: 30 };
345		if (offset.x !== 0) {
346			padding.left += offset.x * 2;
347		}
348		if (offset.y !== 0) {
349			padding.bottom += offset.y * 2;
350		}
351
352		return padding;
353	};
354
355	/**
356	 * Check for map viewing mode (full or with aside)
357	 */
358
359	self.checkMapViewMode = function () {
360		let viewMode = 'normal';
361		if (CossetteApp.$html.hasClass('map-overlay-full')) {
362			viewMode = 'full';
363		} else if (CossetteApp.$html.hasClass('map-overlay-none')) {
364			viewMode = 'none';
365		}
366		return viewMode;
367	};
368
369	/**
370	 * Set map view mode by changing classes on HTML
371	 * @param mode (set viewing mode)
372	 * @param remove (used to remove a defined class)
373	 */
374
375	self.setMapViewMode = function (mode = 'normal', remove = false) {
376		if (remove) {
377			CossetteApp.$html.removeClass(`map-overlay-${mode}`);
378		} else {
379			if (mode !== 'hidden') {
380				CossetteApp.$html.removeClass('map-overlay-full map-overlay-none');
381			}
382			if (mode !== 'normal') {
383				CossetteApp.$html.addClass(`map-overlay-${mode}`);
384			}
385		}
386	};
387
388	/**
389	 * Parse marker markup to build google map markers
390	 * @param elements
391	 * @returns {Array}
392	 */
393	self.parseMarkerData = function (elements) {
394		let data = [];
395
396		$.each(elements, function () {
397			let $this = $(this);
398
399			if (
400				typeof $this.data('lat') == 'undefined' ||
401				typeof $this.data('lng') == 'undefined' ||
402				$this.data('lat') === '' ||
403				$this.data('lng') === ''
404			) {
405				return;
406			}
407
408			let markerData = {
409				coords: { lat: parseFloat($this.data('lat').toFixed(7)), lng: parseFloat($this.data('lng').toFixed(7)) },
410				autoClicked: typeof $this.data('clicked') !== 'undefined'
411			};
412
413			if (typeof $this.data('icon') !== 'undefined') {
414				markerData.icon = {
415					url: $this.data('icon'),
416					size: {
417						width: typeof $this.data('icon-width') !== 'undefined' ? parseInt($this.data('icon-width')) : 48,
418						height: typeof $this.data('icon-height') !== 'undefined' ? parseInt($this.data('icon-height')) : 53
419					}
420				};
421
422				if (document.querySelector('article.remote-iframe') && window.innerWidth >= 2160) {
423					markerData.icon.size.width *= 2.5;
424					markerData.icon.size.height *= 2.5;
425				}
426				// For terminal, use larger marker size
427			} else if (document.querySelector('article.remote-iframe') && window.innerWidth >= 2160) {
428				markerData.customWidth = 66;
429				markerData.customHeight = 72;
430			}
431
432			// Add nodeId to marker data
433			if (typeof $this.data('nid') !== 'undefined') {
434				markerData.nid = $this.data('nid');
435			}
436
437			// Add nodeId to marker data
438			if (typeof $this.data('ajax') !== 'undefined') {
439				markerData.fetchAjax = parseInt($this.data('ajax'));
440			}
441
442			if (typeof $this.data('label') !== 'undefined') {
443				markerData.dayLabel = $this.data('label');
444			}
445
446			// Custom zoom when highlighted
447			if (typeof $this.data('zoom') !== 'undefined') {
448				markerData.zoom = parseInt($this.data('zoom'));
449			}
450
451			// Check if not empty to push html inside infowindow
452			let infoWindowHtml = $this.html();
453			if (infoWindowHtml && infoWindowHtml.toString().trim()) {
454				markerData.infoWindow = infoWindowHtml.toString();
455			}
456
457			data.push(markerData);
458		});
459
460		return data;
461	};
462
463	self.setNeighborhood = function (neighbourhood) {
464		const $containers = document.querySelectorAll('.map-overlay');
465
466		$containers.forEach(function ($container) {
467			$container.querySelectorAll('.marker-trigger').forEach(function ($marker) {
468				$marker.classList.remove('active');
469			});
470
471			if (neighbourhood) {
472				const $neighbourhood = $container.querySelector(`article[data-neighbourhood="${neighbourhood}"]`);
473				if ($neighbourhood) {
474					$neighbourhood.parentElement.classList.add('active');
475				}
476			}
477		});
478	};
479
480	/** ------------------------------
481   * Events
482   --------------------------------- */
483
484	/**
485	 * Map view btn catchall, different behavior based on action
486	 * @param ev
487	 */
488	var onMapBtnClick = function ($button) {
489		let $this = $(this).hasClass('btn-map') ? $(this) : $button;
490		let action = $this[0].hasAttribute('data-action') ? $this.data('action') : 'default';
491		let zones = $this[0].hasAttribute('data-zones') ? $this.data('zones') : null;
492
493		let mapViewMode = self.checkMapViewMode();
494		let offset = self.setMapOffset();
495		let $listItem = $this.closest('.list-item');
496		let startSlideIndex = 0;
497		if ($listItem.length) {
498			startSlideIndex = $listItem.parent().children('.splide__slide').index($listItem);
499			if (startSlideIndex < 0) {
500				startSlideIndex = 0;
501			}
502		}
503
504		let neighbourhood = $this.closest('.list-item').data('neighbourhood');
505		let zoneBounds = true;
506		self.setNeighborhood(neighbourhood);
507
508		switch (action) {
509			case 'enable-list-map':
510				self.enableListMapMode(startSlideIndex);
511				break;
512			case 'disable-list-map':
513				self.disableListMapMode();
514				break;
515			default:
516				let $parent = $this.parent();
517
518				CossetteApp.$html.addClass('has-map-overlay').removeClass('main-nav-sticky main-nav-showing');
519				$parent.addClass('has-map-overlay');
520
521				let mapId = parseInt($this.data('map-id'));
522				self.mapId = mapId;
523
524				if (typeof mapId === 'undefined' || typeof self.$cartographyMaps[mapId] === 'undefined') {
525					let $containers = $parent.find('.map-container');
526
527					if ($containers.length) {
528						loadGoogle().then(function () {
529							let options = {
530								zoom: 15,
531								maxZoom: 18,
532								minZoom: 3,
533								fitToMarker: true,
534								mapTypeControl: false,
535								fullscreenControl: false,
536								gestureHandling: 'greedy',
537								zoomControlOptions: {
538									position: 'LEFT_TOP'
539								}
540							};
541
542							$.each($containers, function (index, value) {
543								let $container = $(this);
544								let $mapSelector = $container.find('.map');
545								let $parent = $this.parent();
546								let $relatedMembers = $container.parents('.related-members');
547								let $markerSelector = $parent.find('.map-marker');
548								let markers = self.parseMarkerData($markerSelector);
549								let must_see_markers = self.parseMarkerData($parent.find('.map-marker-must-see'));
550								zoneBounds = $container.data('behavior') === 'homepage' ? false : true;
551
552								if ($relatedMembers.length) {
553									copyFilters($relatedMembers);
554								}
555
556								if (must_see_markers.length) {
557									options.must_see_markers = must_see_markers;
558								}
559
560								if ($container.get(0).hasAttribute('data-zoom')) {
561									options.initial_zoom = $container.data('zoom');
562								}
563
564								options.$container = $container;
565								let map = new GMap($mapSelector.get(0), options, markers);
566
567								// Charger les zones depuis le bouton et les stocker dans la carte
568								if ($this.get(0).hasAttribute('data-zones')) {
569									let zones = $this.data('zones');
570									if (zones && zones.length > 0) {
571										console.log('Loading zones for map:', zones);
572										map.zones = zones;
573									}
574								}
575
576								self.$cartographyMaps.push(map);
577								$this.data('map-id', self.$cartographyMaps.length - 1);
578
579								const $exploreNavFirstItem = document.querySelector('.explore-nav button[data-index="0"]');
580
581								if (typeof $container.data('behavior') !== 'undefined') {
582									if ($container.data('behavior') === 'bounds') {
583										options.idleCallback = OtqSearch.searchBounds;
584									} else if ($container.data('behavior') === 'nearby') {
585										options.initOnceCallback = OtqSearch.searchNearby;
586									} else if ($container.data('behavior') === 'prefiltered') {
587										options.initOnceCallback = OtqSearch.searchPreFilteredAuto;
588									} else if ($container.data('behavior') === 'homepage') {
589										setTimeout(function () {
590											$exploreNavFirstItem.click();
591										}, 700);
592									}
593								}
594
595								setTimeout(function () {
596									if (mapViewMode === 'normal') {
597										if ($container.data('behavior') !== 'homepage') {
598											self.$cartographyMaps[self.$cartographyMaps.length - 1].handlePan(0, offset);
599										}
600
601										if (zones) {
602											zones.forEach(function (zone) {
603												self.$cartographyMaps[self.$cartographyMaps.length - 1].drawZone(zone, zoneBounds, false, $container.data('behavior'));
604											});
605										}
606									}
607								}, 1000);
608							});
609						});
610					}
611				} else {
612					if (self.clearMarkers) {
613						self.$cartographyMaps[mapId].clearMarkers(true, true);
614						self.clearMarkers = false;
615					}
616
617					let $container = self.$cartographyMaps[mapId].canvas.$container;
618
619					let $relatedMembers = $container.parents('.related-members');
620					if ($relatedMembers.length) {
621						copyFilters($relatedMembers);
622						OtqSearch.searchPreFilteredAuto(self.$cartographyMaps[mapId]);
623					}
624
625					if (mapViewMode === 'normal') {
626						self.$cartographyMaps[mapId].handlePan(0, offset);
627					}
628					if (zones) {
629						zones.forEach(function (zone) {
630							self.$cartographyMaps[mapId].drawZone(zone, zoneBounds);
631						});
632					}
633				}
634
635				break;
636		}
637	};
638
639	/**
640	 * When a sticky marker is clicked, scroll to list item
641	 * @param markerIndex
642	 */
643	var onStickyMarkerClick = function (markerIndex) {
644		if (CossetteApp.$html.hasClass('has-overlay')) {
645			const slideIndex = self.markerIndexToSlideIndex(markerIndex);
646			self.mapSlider.go(slideIndex);
647		} else {
648			let $listItem = $(CossetteApp.$stickyStepsWithMarker[markerIndex]);
649			if ($listItem.length) {
650				CossetteApp.scrollTo($listItem);
651			}
652		}
653	};
654
655	/**
656	 * Resize map overlay on resize click
657	 */
658
659	let onMapResize = function (ev) {
660		ev.preventDefault();
661
662		if (CossetteApp.$html.hasClass('map-overlay-full')) {
663			CossetteApp.$html.addClass('map-overlay-return').removeClass('map-overlay-full');
664			setTimeout(function () {
665				CossetteApp.$html.removeClass('map-overlay-return');
666			}, 650);
667		} else {
668			CossetteApp.$html.addClass('map-overlay-full');
669		}
670	};
671
672	let onLocationClick = function (ev) {
673		ev.preventDefault();
674
675		let $this = $(this),
676			$overlay = $this.parents('.map-overlay'),
677			mapId = $overlay.siblings('.btn-map').data('map-id');
678
679		function success(position) {
680			const latitude = position.coords.latitude;
681			const longitude = position.coords.longitude;
682
683			if (typeof self.$cartographyMaps[mapId] !== 'undefined') {
684				self.$cartographyMaps[mapId].canvas.setCenter(new google.maps.LatLng(latitude, longitude));
685			}
686		}
687
688		if (navigator.geolocation) {
689			navigator.geolocation.getCurrentPosition(success, () => {});
690		}
691	};
692
693	/**
694	 * On marker trigger click
695	 */
696	var onMarkerTriggerClick = function (e) {
697		if (!e.target.closest('.marker-link')) {
698			e.preventDefault();
699
700			let $this = $(this);
701			let mapId = parseInt($this.parents('.map-overlay').siblings('.btn-map').data('map-id'));
702			let nid = $this.data('nid');
703
704			if (typeof self.$cartographyMaps[mapId] !== 'undefined') {
705				if ($this.hasClass('marker-latlng') && $this.data('lat') && $this.data('lng')) {
706					self.$cartographyMaps[mapId].zoomOnLatLng($this.data('lat'), $this.data('lng'));
707				} else if ($this.hasClass('marker-zone')) {
708					self.$cartographyMaps[mapId].doActionOnMarkerZone(nid, $this.find('.hidden-infowindow').html());
709					console.log('voici le html', $this.find('.hidden-infowindow').html());
710				} else if ($this.hasClass('marker-subzone')) {
711					const zones = self.$cartographyMaps[mapId].zones;
712					const neighbourhood = $this.find('.teaser').data('neighbourhood');
713
714					if (zones.length && neighbourhood && zones[0] && zones[0].subzones) {
715						const subZone = zones[0].subzones.find((subzone) => parseInt(subzone.id) === parseInt(neighbourhood));
716						if (subZone) {
717							self.$cartographyMaps[mapId].drawSubzone(subZone);
718						}
719					}
720				} else {
721					self.$cartographyMaps[mapId].doActionOnMarkerNid(nid, 'click');
722				}
723
724				if ($this.hasClass('marker-overlay-return')) {
725					self.setMapViewMode('none', true);
726				}
727			}
728		}
729	};
730
731	/**
732	 * On marker trigger click
733	 */
734	var onMarkerEnter = function (e) {
735		e.preventDefault();
736		let $this = $(this);
737		let mapId = parseInt($this.parents('.map-overlay').siblings('.btn-map').data('map-id'));
738		console.log('onMarkerEnter', mapId);
739		let nid = $this.data('nid');
740
741		if (typeof self.$cartographyMaps[mapId] !== 'undefined') {
742			self.$cartographyMaps[mapId].doActionOnMarkerNid(nid, 'enter');
743		}
744	};
745
746	/**
747	 * On marker trigger click
748	 */
749	var onMarkerLeave = function (e) {
750		e.preventDefault();
751		let $this = $(this);
752		let mapId = parseInt($this.parents('.map-overlay').siblings('.btn-map').data('map-id'));
753		let nid = $this.data('nid');
754
755		if (typeof self.$cartographyMaps[mapId] !== 'undefined') {
756			self.$cartographyMaps[mapId].doActionOnMarkerNid(nid, 'leave');
757		}
758	};
759
760	var onPaginationClick = function (ev) {
761		let $this = $(this),
762			$container = $overlay.find('.map-container'),
763			page = parseInt($this.data('page')),
764			mapId = parseInt($overlay.siblings('.btn-map').data('map-id'));
765
766		clearTimeout(pageClickDelay);
767		pageClickDelay = setTimeout(function () {
768			if ($container.data('behavior') === 'nearby') {
769				OtqSearch.searchNearby(self.$cartographyMaps[mapId], page);
770			} else if ($container.data('behavior') === 'bounds') {
771				OtqSearch.searchBounds(self.$cartographyMaps[mapId], page);
772			}
773		}, 250);
774	};
775
776	var onLeaveInfowindow = function () {
777		$mapMarkerAjax.removeClass('selected');
778		self.setMapViewMode('hidden', true);
779	};
780
781	var onExploreNavClick = function (ev) {
782		ev.preventDefault();
783		self.changeExploreNav(ev.target);
784
785		let $this = $(this),
786			zoom = $this.data('zoom'),
787			mapId = parseInt($overlay.siblings('.btn-map').data('map-id'));
788
789		if (typeof self.$cartographyMaps[mapId] !== 'undefined') {
790			if (zoom === 'bounds') {
791				self.$cartographyMaps[mapId].fitToMarkers();
792			} else {
793				zoom = parseInt(zoom);
794				self.$cartographyMaps[mapId].canvas.setZoom(zoom);
795				self.$cartographyMaps[mapId].centerOnCenter();
796				zonesCleared = false;
797			}
798
799			if ($this.data('clear-zones') && !zonesCleared) {
800				self.$cartographyMaps[mapId].removeFeatures();
801				zonesCleared = true;
802			} else {
803				// Dessiner les zones appropriées selon le zoom
804				self.drawZonesForZoom(mapId, zoom);
805			}
806		}
807	};
808
809	self.drawZonesForZoom = function (mapId, zoom) {
810		let map = self.$cartographyMaps[mapId];
811		if (!map || !map.zones) return;
812
813		// Effacer toutes les zones existantes
814		map.removeFeatures();
815
816		// Trouver et dessiner les zones appropriées pour ce niveau de zoom
817		map.zones.forEach(function (zone) {
818			if (zone.zoom && zoom >= zone.zoom.start && zoom <= zone.zoom.end) {
819				console.log('Drawing zone for zoom', zoom, ':', zone.id);
820				map.drawZone(zone, false);
821			}
822		});
823	};
824
825	self.changeExploreNav = function (button) {
826		if (button.classList.contains('active')) {
827			return;
828		}
829
830		let $exploreNav = button.closest('.explore-nav');
831		let $overlay = button.closest('.map-overlay');
832		let $follow = $exploreNav.querySelector('.follow');
833		let $allButtons = $exploreNav.querySelectorAll('button');
834
835		$allButtons.forEach(function ($button) {
836			$button.classList.remove('active');
837		});
838
839		button.classList.add('active');
840
841		const buttonBounds = button.getBoundingClientRect();
842		const parentBounds = $exploreNav.getBoundingClientRect();
843		const relativeLeft = buttonBounds.left - parentBounds.left;
844		$follow.style.transform = `translateX(${relativeLeft}px)`;
845		$follow.style.width = `${buttonBounds.width}px`;
846
847		const $mapSections = $overlay.querySelectorAll('.map-section');
848		$mapSections.forEach(function (section) {
849			if (section.dataset.index === button.dataset.index) {
850				section.classList.add('active');
851			} else {
852				section.classList.remove('active');
853			}
854		});
855	};
856
857	/**
858	 * Copy Filters from paragraph to embedded map, that way it displays always the same
859	 * @param $container
860	 */
861	var copyFilters = function ($container) {
862		// Copy filters
863		let $filters = $container.find('.filters-container');
864
865		if ($filters.length === 2) {
866			if ($($filters[0]).find('.orphan-filters').length) {
867				$($filters[1]).find('.orphan-filter.selected').trigger('click');
868
869				$($filters[0])
870					.find('input[type=checkbox]:checked')
871					.each(function () {
872						let $this = $(this);
873						$($filters[1])
874							.find('input[name="' + $this.attr('name') + '"][value=' + $this.attr('value') + ']')
875							.siblings('.btn-filter')
876							.trigger('click');
877					});
878			} else {
879				$($filters[1]).find('input[type=checkbox]:checked').prop('checked', false).trigger('change');
880				$($filters[0])
881					.find('input[type=checkbox]:checked')
882					.each(function () {
883						let $this = $(this);
884						$($filters[1])
885							.find('input[name="' + $this.attr('name') + '"][value=' + $this.attr('value') + ']')
886							.prop('checked', true)
887							.trigger('change');
888					});
889			}
890		}
891
892		// Copy input
893		let $searchInputs = $container.find('input[name=q]');
894
895		if ($searchInputs.length === 2) {
896			$($searchInputs[1]).val($($searchInputs[0]).val());
897		}
898	};
899
900	var onCloseNeighbourhood = function (ev) {
901		ev.preventDefault();
902		let $this = $(this);
903		let $overlay = $this.closest('.map-overlay');
904		let $cta = $this.closest('.cta-map');
905		let $neighbourhood = $overlay.find('.marker-trigger.active');
906		$neighbourhood.removeClass('active');
907		self.clearMarkers = true;
908		onMapBtnClick($cta.find('.btn-map'));
909	};
910
911	/** ------------------------------
912   * Private methods
913   --------------------------------- */
914	let loadGoogle = function () {
915		return new Promise((resolve, reject) => {
916			if (mapScriptLoaded) {
917				resolve('complete');
918			} else {
919				// This code loads the code asynchronously.
920				var tag = document.createElement('script');
921
922				tag.src = '//maps.googleapis.com/maps/api/js?v=3&region=CA&key=' + Globals.mapApiKey + '&callback=gmapReady&libraries=geometry';
923				var firstScriptTag = document.getElementsByTagName('script')[0];
924				firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
925
926				mapScriptLoaded = true;
927
928				let interval = setInterval(() => {
929					if (CossetteApp.$body.hasClass('gmap-ready')) {
930						resolve('complete');
931						clearInterval(interval);
932					}
933				}, 50);
934			}
935		});
936	};
937
938	let initStickyGmap = function ($container) {
939		if ($container.length) {
940			loadGoogle().then(function () {
941				let options = {
942					$container: $container,
943					zoom: 8,
944					maxZoom: 18,
945					minZoom: 3,
946					mapTypeControl: false,
947					fullscreenControl: false,
948					gestureHandling: 'greedy',
949					zoomControlOptions: {
950						position: 'LEFT_TOP'
951					},
952					handleMarkerCallback: onStickyMarkerClick
953				};
954
955				let $mapSelector = $container.find('.map');
956				let $markerSelector = $container.find('.map-marker');
957
958				let markers = self.parseMarkerData($markerSelector);
959				let must_see_markers = self.parseMarkerData(self.$listContentWrapper.find('.map-marker-must-see'));
960
961				if (must_see_markers.length) {
962					options.must_see_markers = must_see_markers;
963				}
964
965				let map = new GMap($mapSelector.get(0), options, markers);
966
967				CossetteApp.maps.push(map);
968
969				if (CossetteApp.$stickyOffset && CossetteApp.$stickyOffset.length) {
970					let gMapInterval = setInterval(function () {
971						if (typeof CossetteApp.maps[0] !== 'undefined' && CossetteApp.maps[0].loaded === true) {
972							clearInterval(gMapInterval);
973							CossetteApp.stickyMapLoaded = true;
974
975							// Set scroll from start
976							let gMapInterval2 = setInterval(function () {
977								clearInterval(gMapInterval2);
978
979								let currentScroll =
980									window.scrollY ||
981									window.pageYOffset ||
982									document.body.scrollTop + ((document.documentElement && document.documentElement.scrollTop) || 0);
983								self.adjustMapStickyness(currentScroll);
984								CossetteRouter.initiliazedClasses['OtqSlider'].checkListSlider();
985							}, 300);
986						}
987					}, 100);
988				}
989			});
990		}
991	};
992
993	/**
994	 * Set current active list item
995	 * @param currentScroll
996	 */
997	var setStickyStepState = function (currentScroll) {
998		let $currentStep = CossetteApp.$stickySteps.closestToOffset({ left: 0, top: currentScroll });
999
1000		if ($currentStep.attr('data-has-marker') === '1' && !$currentStep.hasClass('is-active')) {
1001			let markerIndex = CossetteApp.$stickyStepsWithMarker.index($currentStep);
1002
1003			if (CossetteApp.maps.length) {
1004				CossetteApp.maps[0].highlightCurrentMarker(markerIndex);
1005				CossetteApp.maps[0].handlePan(markerIndex);
1006			}
1007
1008			CossetteApp.$stickySteps.removeClass('is-active');
1009			$currentStep.addClass('is-active');
1010		}
1011	};
1012
1013	/**
1014	 * Highlight last marker when past the list
1015	 */
1016	var setLastStickyStepState = function () {
1017		if (!CossetteApp.maps[0]) return;
1018
1019		let $currentStep = CossetteApp.$stickySteps.last();
1020
1021		if (!$currentStep.hasClass('is-active')) {
1022			let markerIndex = $currentStep.index() - 1;
1023
1024			CossetteApp.maps[0].highlightCurrentMarker(markerIndex);
1025			CossetteApp.maps[0].handlePan(markerIndex);
1026
1027			CossetteApp.$stickySteps.removeClass('is-active');
1028
1029			$currentStep.addClass('is-active');
1030		}
1031	};
1032
1033	/** ------------------------------
1034   * Constructor/Deconstructor
1035   --------------------------------- */
1036
1037	self.construct = (function () {})();
1038
1039	return self;
1040};

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.