Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
1/**
2 * Location Map Viewer Component
3 * Handles read-only map display for event viewing pages
4 */
5class LocationMapViewer {
6 constructor(containerId, options = {}) {
7 this.containerId = containerId;
8 this.container = document.getElementById(containerId);
9 this.options = {
10 defaultZoom: 15,
11 maxZoom: 18,
12 ...options
13 };
14 this.map = null;
15 this.marker = null;
16
17 if (this.container) {
18 this.initializeMap();
19 }
20 }
21
22 async initializeMap() {
23 try {
24 this.showLoading();
25
26 const lat = parseFloat(this.container.dataset.lat);
27 const lng = parseFloat(this.container.dataset.lng);
28 const address = this.container.dataset.address;
29 const venueName = this.container.dataset.venueName;
30
31 if (lat && lng) {
32 this.createMapWithCoordinates(lat, lng, address, venueName);
33 } else if (address) {
34 await this.createMapWithGeocoding(address);
35 } else {
36 this.showError('No location data available');
37 }
38 } catch (error) {
39 console.error('Map initialization error:', error);
40 this.showError('Failed to load map');
41 }
42 }
43
44 createMapWithCoordinates(lat, lng, address, venueName) {
45 try {
46 // Initialize MapLibreGL map
47 if (typeof maplibregl === 'undefined') {
48 this.showFallback(lat, lng, venueName || address || 'Event Location');
49 return;
50 }
51
52 this.map = new maplibregl.Map({
53 container: this.containerId,
54 style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
55 center: [lng, lat],
56 zoom: this.options.defaultZoom,
57 scrollZoom: false,
58 dragPan: true,
59 touchZoomRotate: true,
60 doubleClickZoom: true,
61 boxZoom: false,
62 keyboard: false
63 });
64
65 // Add marker with midnight glass theme
66 this.marker = new maplibregl.Marker({
67 color: '#7877c6'
68 })
69 .setLngLat([lng, lat])
70 .addTo(this.map);
71
72 // Add popup with venue information
73 const popupContent = this.createPopupContent(venueName || address || 'Event Location', address);
74 const popup = new maplibregl.Popup()
75 .setHTML(popupContent);
76 this.marker.setPopup(popup);
77
78 // Add navigation controls (zoom buttons)
79 this.map.addControl(new maplibregl.NavigationControl(), 'top-right');
80
81 // Apply midnight glass theme
82 this.map.on('load', () => {
83 this.applyMidnightGlassTheme(this.map);
84 });
85
86 this.hideLoading();
87 } catch (error) {
88 console.error('Error creating map with coordinates:', error);
89 this.showError('Failed to create map');
90 }
91 }
92
93 async createMapWithGeocoding(address) {
94 try {
95 // Try multiple fallback strategies for geocoding
96 const searchStrategies = this.generateGeocodingFallbacks(address);
97
98 for (let i = 0; i < searchStrategies.length; i++) {
99 const searchAddress = searchStrategies[i];
100 console.log(`Trying geocoding strategy ${i + 1}/${searchStrategies.length}: ${searchAddress}`);
101
102 const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchAddress)}&limit=1`);
103 const results = await response.json();
104
105 if (results && results.length > 0) {
106 const result = results[0];
107 const lat = parseFloat(result.lat);
108 const lng = parseFloat(result.lon);
109
110 console.log(`Geocoding successful with strategy ${i + 1}: ${searchAddress} -> ${lat}, ${lng}`);
111 this.createMapWithCoordinates(lat, lng, address);
112 return; // Success - exit early
113 }
114 }
115
116 // If all strategies failed, show error
117 console.error('All geocoding strategies failed for address:', address);
118 this.showError('Location not found - address may be too specific or incomplete');
119 } catch (error) {
120 console.error('Geocoding error:', error);
121 this.showError('Failed to find location');
122 }
123 }
124
125 generateGeocodingFallbacks(address) {
126 const fallbacks = [];
127
128 // Strategy 1: Use original address
129 fallbacks.push(address);
130
131 // Strategy 2: Replace province codes with full names
132 let normalizedAddress = address
133 .replace(/\b(QC|PQ)\b/gi, 'Quebec')
134 .replace(/\b(ON|ONT)\b/gi, 'Ontario')
135 .replace(/\b(BC|CB)\b/gi, 'British Columbia')
136 .replace(/\b(AB|ALTA)\b/gi, 'Alberta')
137 .replace(/\b(MB|MAN)\b/gi, 'Manitoba')
138 .replace(/\b(SK|SASK)\b/gi, 'Saskatchewan')
139 .replace(/\b(NS|NE)\b/gi, 'Nova Scotia')
140 .replace(/\b(NB)\b/gi, 'New Brunswick')
141 .replace(/\b(PE|PEI)\b/gi, 'Prince Edward Island')
142 .replace(/\b(NL|NF)\b/gi, 'Newfoundland');
143
144 if (normalizedAddress !== address) {
145 fallbacks.push(normalizedAddress);
146 }
147
148 // Strategy 3: Remove specific building/venue name, keep rest
149 const parts = address.split(',').map(p => p.trim());
150 if (parts.length > 2) {
151 const withoutBuilding = parts.slice(1).join(', ');
152 fallbacks.push(withoutBuilding);
153
154 // Also try normalized version without building
155 const normalizedWithoutBuilding = withoutBuilding
156 .replace(/\b(QC|PQ)\b/gi, 'Quebec')
157 .replace(/\b(ON|ONT)\b/gi, 'Ontario')
158 .replace(/\b(BC|CB)\b/gi, 'British Columbia');
159
160 if (normalizedWithoutBuilding !== withoutBuilding) {
161 fallbacks.push(normalizedWithoutBuilding);
162 }
163 }
164
165 // Strategy 4: City, Province, Country only
166 if (parts.length >= 3) {
167 const cityProvinceCountry = [parts[parts.length - 3], parts[parts.length - 2], parts[parts.length - 1]].join(', ');
168 fallbacks.push(cityProvinceCountry);
169
170 // Normalize province in city/province/country
171 const normalizedCityProvince = cityProvinceCountry
172 .replace(/\b(QC|PQ)\b/gi, 'Quebec')
173 .replace(/\b(ON|ONT)\b/gi, 'Ontario');
174
175 if (normalizedCityProvince !== cityProvinceCountry) {
176 fallbacks.push(normalizedCityProvince);
177 }
178 }
179
180 // Strategy 5: Province, Country only
181 if (parts.length >= 2) {
182 const provinceCountry = [parts[parts.length - 2], parts[parts.length - 1]].join(', ');
183 fallbacks.push(provinceCountry);
184
185 const normalizedProvince = provinceCountry.replace(/\b(QC|PQ)\b/gi, 'Quebec');
186 if (normalizedProvince !== provinceCountry) {
187 fallbacks.push(normalizedProvince);
188 }
189 }
190
191 // Strategy 6: Country only
192 if (parts.length >= 1) {
193 fallbacks.push(parts[parts.length - 1]);
194 }
195
196 // Remove duplicates while preserving order
197 return [...new Set(fallbacks)];
198 }
199
200 createPopupContent(title, address) {
201 return `
202 <div class="venue-popup-content">
203 <h4 class="title is-6 mb-2">${title}</h4>
204 ${address ? `<p class="subtitle is-7 mb-2">${address}</p>` : ''}
205 <div class="buttons are-small">
206 <a class="button is-link is-small" href="https://maps.apple.com/?q=${encodeURIComponent(address || title)}" target="_blank" rel="noopener">
207 <span class="icon"><i class="fab fa-apple"></i></span>
208 <span>Apple Maps</span>
209 </a>
210 <a class="button is-link is-small" href="https://maps.google.com/?q=${encodeURIComponent(address || title)}" target="_blank" rel="noopener">
211 <span class="icon"><i class="fab fa-google"></i></span>
212 <span>Google Maps</span>
213 </a>
214 </div>
215 </div>
216 `;
217 }
218
219 showLoading() {
220 const loading = document.getElementById('event-map-loading');
221 if (loading) {
222 loading.classList.remove('is-hidden');
223 }
224 }
225
226 hideLoading() {
227 const loading = document.getElementById('event-map-loading');
228 if (loading) {
229 loading.classList.add('is-hidden');
230 }
231 }
232
233 showError(message) {
234 this.hideLoading();
235 const error = document.getElementById('event-map-error');
236 if (error) {
237 error.textContent = message;
238 error.classList.remove('is-hidden');
239 }
240
241 // Hide the map container
242 if (this.container) {
243 this.container.style.display = 'none';
244 }
245 }
246
247 showFallback(lat, lng, title) {
248 this.container.innerHTML = `
249 <div class="map-fallback" style="height: ${this.options.height}px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border: 1px solid #ddd; border-radius: 6px;">
250 <div class="has-text-centered">
251 <span class="icon is-large"><i class="fas fa-map-marker-alt fa-2x"></i></span><br>
252 <strong>${title}</strong><br>
253 <small>${lat.toFixed(4)}, ${lng.toFixed(4)}</small>
254 </div>
255 </div>
256 `;
257 this.hideLoading();
258 }
259
260 applyMidnightGlassTheme(map) {
261 // Fond et terre
262 if(map.getLayer('background')) {
263 map.setPaintProperty('background', 'background-color', '#1a1b2a');
264 }
265 if(map.getLayer('landcover')) {
266 map.setPaintProperty('landcover', 'fill-color', '#2e2f49');
267 }
268 if(map.getLayer('landuse_residential')) {
269 map.setPaintProperty('landuse_residential', 'fill-color', '#3a3b5e');
270 map.setPaintProperty('landuse_residential', 'fill-opacity', 0.4);
271 }
272 if(map.getLayer('landuse')) {
273 map.setPaintProperty('landuse', 'fill-color', '#3a3b5e');
274 map.setPaintProperty('landuse', 'fill-opacity', 0.3);
275 }
276
277 // Eau et ombres
278 if(map.getLayer('water')) {
279 map.setPaintProperty('water', 'fill-color', [
280 'interpolate', ['linear'], ['zoom'],
281 0, '#2b2f66',
282 10, '#5b5ea6',
283 15, '#8779c3'
284 ]);
285 map.setPaintProperty('water', 'fill-opacity', 0.85);
286 }
287 if(map.getLayer('water_shadow')) {
288 map.setPaintProperty('water_shadow', 'fill-color', '#555a9a');
289 map.setPaintProperty('water_shadow', 'fill-opacity', 0.3);
290 }
291
292 // Parcs
293 ['park_national_park', 'park_nature_reserve'].forEach(id => {
294 if(map.getLayer(id)) {
295 map.setPaintProperty(id, 'fill-color', '#50537a');
296 map.setPaintProperty(id, 'fill-opacity', 0.3);
297 }
298 });
299
300 // Routes principales et secondaires
301 const roadsPrimary = [
302 'road_pri_case_noramp', 'road_pri_fill_noramp',
303 'road_pri_case_ramp', 'road_pri_fill_ramp'
304 ];
305 roadsPrimary.forEach(id => {
306 if(map.getLayer(id)) {
307 map.setPaintProperty(id, 'line-color', '#9389b8');
308 if(id.includes('fill')) {
309 map.setPaintProperty(id, 'line-width', 2);
310 }
311 }
312 });
313
314 const roadsSecondary = [
315 'road_sec_case_noramp', 'road_sec_fill_noramp'
316 ];
317 roadsSecondary.forEach(id => {
318 if(map.getLayer(id)) {
319 map.setPaintProperty(id, 'line-color', '#6d6ea1');
320 if(id.includes('fill')) {
321 map.setPaintProperty(id, 'line-width', 1.5);
322 }
323 }
324 });
325
326 // Bâtiments
327 ['building', 'building-top'].forEach(id => {
328 if(map.getLayer(id)) {
329 map.setPaintProperty(id, 'fill-color', '#9a92bc');
330 map.setPaintProperty(id, 'fill-opacity', 0.35);
331 }
332 });
333
334 // Ponts
335 ['bridge_pri_case', 'bridge_pri_fill', 'bridge_sec_case', 'bridge_sec_fill'].forEach(id => {
336 if(map.getLayer(id)) {
337 map.setPaintProperty(id, 'line-color', '#7a75aa');
338 }
339 });
340 }
341}
342
343// Mini Map Viewer for event cards and venue previews
344class MiniMapViewer {
345 constructor(container, options = {}) {
346 this.container = container;
347 this.options = {
348 defaultZoom: 14,
349 height: 150,
350 ...options
351 };
352 this.map = null;
353
354 if (this.container) {
355 this.initializeMiniMap();
356 }
357 }
358
359 initializeMiniMap() {
360 try {
361 const lat = parseFloat(this.container.dataset.lat);
362 const lng = parseFloat(this.container.dataset.lng);
363 const venueName = this.container.dataset.venueName;
364
365 if (!lat || !lng) {
366 this.showError('No coordinates available');
367 return;
368 }
369
370 // Set height
371 this.container.style.height = `${this.options.height}px`;
372
373 // Initialize mini map
374 if (typeof maplibregl === 'undefined') {
375 this.showFallback(lat, lng, venueName);
376 return;
377 }
378
379 this.map = new maplibregl.Map({
380 container: this.container,
381 style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
382 center: [lng, lat],
383 zoom: this.options.defaultZoom,
384 interactive: false, // Disable all interactions for mini map
385 attributionControl: false
386 });
387
388 // Add marker with midnight glass theme
389 const marker = new maplibregl.Marker({
390 color: '#7877c6'
391 })
392 .setLngLat([lng, lat])
393 .addTo(this.map);
394
395 // Add click handler to open full view
396 this.container.addEventListener('click', () => {
397 this.openFullMap(lat, lng, venueName);
398 });
399
400 this.container.style.cursor = 'pointer';
401 this.container.title = 'Click to view full map';
402
403 // Apply midnight glass theme
404 this.map.on('load', () => {
405 this.applyMidnightGlassTheme(this.map);
406 });
407
408 this.hideLoading();
409 } catch (error) {
410 console.error('Error creating mini map:', error);
411 this.showError('Map unavailable');
412 }
413 }
414
415 openFullMap(lat, lng, venueName) {
416 // Get translations from body data attributes
417 const defaultTitle = document.body.dataset.i18nMapDefaultTitle || 'Location';
418 const closeText = document.body.dataset.i18nClose || 'Close';
419 const openMapText = document.body.dataset.i18nOpenOpenstreetmap || 'Open in OpenStreetMap';
420
421 // Create modal for full map view
422 const modal = document.createElement('div');
423 modal.className = 'modal is-active';
424 modal.innerHTML = `
425 <div class="modal-background" onclick="this.parentElement.remove()"></div>
426 <div class="modal-content">
427 <div class="box">
428 <h3 class="title is-4">${venueName || defaultTitle}</h3>
429 <div id="full-map-${Date.now()}" style="height: 400px; width: 100%;"></div>
430 <div class="mt-4">
431 <button class="button" onclick="this.closest('.modal').remove()">${closeText}</button>
432 <a href="https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}&zoom=16"
433 target="_blank" class="button is-info">
434 <span class="icon"><i class="fas fa-external-link-alt"></i></span>
435 <span>${openMapText}</span>
436 </a>
437 </div>
438 </div>
439 </div>
440 <button class="modal-close is-large" onclick="this.parentElement.remove()"></button>
441 `;
442
443 document.body.appendChild(modal);
444
445 // Initialize full map
446 const mapId = modal.querySelector('[id^="full-map-"]').id;
447 setTimeout(() => {
448 if (typeof maplibregl !== 'undefined') {
449 try {
450 const fullMap = new maplibregl.Map({
451 container: mapId,
452 style: {
453 version: 8,
454 sources: {
455 'osm': {
456 type: 'raster',
457 tiles: [
458 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
459 'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
460 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'
461 ],
462 tileSize: 256,
463 attribution: '© OpenStreetMap contributors'
464 }
465 },
466 layers: [
467 {
468 id: 'osm',
469 type: 'raster',
470 source: 'osm'
471 }
472 ]
473 },
474 center: [lng, lat],
475 zoom: 16,
476 attributionControl: true
477 });
478
479 new maplibregl.Marker({
480 color: '#7877c6'
481 })
482 .setLngLat([lng, lat])
483 .addTo(fullMap);
484
485 // Apply midnight glass theme
486 fullMap.on('load', () => {
487 this.applyMidnightGlassTheme(fullMap);
488 });
489 } catch (error) {
490 console.error('Error creating full map:', error);
491 }
492 }
493 }, 100);
494 }
495
496 hideLoading() {
497 const loading = this.container.querySelector('.map-loading');
498 if (loading) {
499 loading.style.display = 'none';
500 }
501 }
502
503 showError(message) {
504 this.hideLoading();
505 this.container.innerHTML = `
506 <div class="map-error has-text-centered has-text-grey">
507 <span class="icon"><i class="fas fa-map"></i></span>
508 <p>${message}</p>
509 </div>
510 `;
511 }
512
513 showFallback(lat, lng, title) {
514 this.container.innerHTML = `
515 <div class="map-fallback" style="height: ${this.options.height}px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border: 1px solid #ddd; border-radius: 6px;">
516 <div class="has-text-centered">
517 <span class="icon is-large"><i class="fas fa-map-marker-alt fa-2x"></i></span><br>
518 <strong>${title}</strong><br>
519 <small>${lat.toFixed(4)}, ${lng.toFixed(4)}</small>
520 </div>
521 </div>
522 `;
523 }
524
525 applyMidnightGlassTheme(map) {
526 // Fond et terre
527 if(map.getLayer('background')) {
528 map.setPaintProperty('background', 'background-color', '#1a1b2a');
529 }
530 if(map.getLayer('landcover')) {
531 map.setPaintProperty('landcover', 'fill-color', '#2e2f49');
532 }
533 if(map.getLayer('landuse_residential')) {
534 map.setPaintProperty('landuse_residential', 'fill-color', '#3a3b5e');
535 map.setPaintProperty('landuse_residential', 'fill-opacity', 0.4);
536 }
537 if(map.getLayer('landuse')) {
538 map.setPaintProperty('landuse', 'fill-color', '#3a3b5e');
539 map.setPaintProperty('landuse', 'fill-opacity', 0.3);
540 }
541
542 // Eau et ombres
543 if(map.getLayer('water')) {
544 map.setPaintProperty('water', 'fill-color', [
545 'interpolate', ['linear'], ['zoom'],
546 0, '#2b2f66',
547 10, '#5b5ea6',
548 15, '#8779c3'
549 ]);
550 map.setPaintProperty('water', 'fill-opacity', 0.85);
551 }
552 if(map.getLayer('water_shadow')) {
553 map.setPaintProperty('water_shadow', 'fill-color', '#555a9a');
554 map.setPaintProperty('water_shadow', 'fill-opacity', 0.3);
555 }
556
557 // Parcs
558 ['park_national_park', 'park_nature_reserve'].forEach(id => {
559 if(map.getLayer(id)) {
560 map.setPaintProperty(id, 'fill-color', '#50537a');
561 map.setPaintProperty(id, 'fill-opacity', 0.3);
562 }
563 });
564
565 // Routes principales et secondaires
566 const roadsPrimary = [
567 'road_pri_case_noramp', 'road_pri_fill_noramp',
568 'road_pri_case_ramp', 'road_pri_fill_ramp'
569 ];
570 roadsPrimary.forEach(id => {
571 if(map.getLayer(id)) {
572 map.setPaintProperty(id, 'line-color', '#9389b8');
573 if(id.includes('fill')) {
574 map.setPaintProperty(id, 'line-width', 2);
575 }
576 }
577 });
578
579 const roadsSecondary = [
580 'road_sec_case_noramp', 'road_sec_fill_noramp'
581 ];
582 roadsSecondary.forEach(id => {
583 if(map.getLayer(id)) {
584 map.setPaintProperty(id, 'line-color', '#6d6ea1');
585 if(id.includes('fill')) {
586 map.setPaintProperty(id, 'line-width', 1.5);
587 }
588 }
589 });
590
591 // Bâtiments
592 ['building', 'building-top'].forEach(id => {
593 if(map.getLayer(id)) {
594 map.setPaintProperty(id, 'fill-color', '#9a92bc');
595 map.setPaintProperty(id, 'fill-opacity', 0.35);
596 }
597 });
598
599 // Ponts
600 ['bridge_pri_case', 'bridge_pri_fill', 'bridge_sec_case', 'bridge_sec_fill'].forEach(id => {
601 if(map.getLayer(id)) {
602 map.setPaintProperty(id, 'line-color', '#7a75aa');
603 }
604 });
605 }
606}
607
608// Export for global access
609window.MiniMapViewer = MiniMapViewer;
610
611// Global map picker function for location forms
612window.openMapPicker = function() {
613 // Get translations from body data attributes
614 const pickerTitle = document.body.dataset.i18nMapPickerTitle || 'Choose Location on Map';
615 const pickerInstructions = document.body.dataset.i18nMapPickerInstructions || 'Click on the map to select a location';
616 const useLocationText = document.body.dataset.i18nUseLocation || 'Use This Location';
617 const closeText = document.body.dataset.i18nClose || 'Close';
618 const locateMeText = document.body.dataset.i18nLocateMe || 'Locate Me';
619 const locatingUserText = document.body.dataset.i18nLocatingUser || 'Locating your position...';
620 const locationErrorText = document.body.dataset.i18nLocationError || 'Unable to access your location';
621 const locationPermissionDeniedText = document.body.dataset.i18nLocationPermissionDenied || 'Location access denied';
622
623 // Create modal for map picker
624 const modal = document.createElement('div');
625 modal.className = 'modal is-active';
626 modal.id = 'map-picker-modal';
627
628 const mapId = 'map-picker-' + Date.now();
629 modal.innerHTML = `
630 <div class="modal-background" onclick="closeMapPicker()"></div>
631 <div class="modal-content">
632 <div class="box">
633 <h3 class="title is-4">${pickerTitle}</h3>
634 <p class="subtitle is-6">${pickerInstructions}</p>
635 <div class="mb-3">
636 <button class="button is-info is-small" id="locate-me-btn" onclick="locateUser()">
637 <span class="icon">
638 <i class="fas fa-location-arrow"></i>
639 </span>
640 <span>${locateMeText}</span>
641 </button>
642 <div id="location-status" class="help" style="display: none;"></div>
643 </div>
644 <div id="${mapId}" style="height: 400px; width: 100%; cursor: crosshair;"></div>
645 <div class="mt-4 is-flex is-justify-content-space-between">
646 <button class="button" onclick="closeMapPicker()">${closeText}</button>
647 <button class="button is-primary" id="use-location-btn" onclick="useSelectedLocation()" disabled>
648 ${useLocationText}
649 </button>
650 </div>
651 </div>
652 </div>
653 <button class="modal-close is-large" onclick="closeMapPicker()"></button>
654 `;
655
656 document.body.appendChild(modal);
657
658 let selectedLocation = null;
659 let marker = null;
660 let map = null;
661
662 // Initialize map
663 setTimeout(() => {
664 if (typeof maplibregl !== 'undefined') {
665 try {
666 map = new maplibregl.Map({
667 container: mapId,
668 style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
669 center: [-73.5673, 45.5017], // Default to Montreal
670 zoom: 10,
671 attributionControl: true
672 });
673
674 // Apply midnight glass theme
675 map.on('load', () => {
676 if (window.MiniMapViewer && typeof window.MiniMapViewer.prototype.applyMidnightGlassTheme === 'function') {
677 window.MiniMapViewer.prototype.applyMidnightGlassTheme.call({ map }, map);
678 }
679 });
680
681 // Add click handler for location selection
682 map.on('click', async (e) => {
683 await selectLocation(e.lngLat.lng, e.lngLat.lat);
684 });
685
686 // Store map reference for cleanup
687 window.mapPickerInstance = {
688 map,
689 marker,
690 selectedLocation,
691 selectLocation,
692 translations: {
693 locatingUserText,
694 locationErrorText,
695 locationPermissionDeniedText
696 }
697 };
698
699 } catch (error) {
700 console.error('Error creating map picker:', error);
701 document.getElementById(mapId).innerHTML = `
702 <div class="notification is-warning">
703 <p>Map could not be loaded. Please enter the location manually.</p>
704 </div>
705 `;
706 }
707 }
708 }, 100);
709
710 // Function to select a location (used by both click and geolocation)
711 async function selectLocation(lng, lat) {
712 // Remove existing marker
713 if (marker) {
714 marker.remove();
715 }
716
717 // Add new marker
718 marker = new maplibregl.Marker({
719 color: '#7877c6'
720 })
721 .setLngLat([lng, lat])
722 .addTo(map);
723
724 // Store selected location
725 selectedLocation = { lat, lng };
726
727 // Enable use location button
728 document.getElementById('use-location-btn').disabled = false;
729
730 // Try to reverse geocode for address
731 try {
732 const response = await fetch(`http://localhost:8080/reverse?format=json&lat=${lat}&lon=${lng}&zoom=16&addressdetails=1`);
733 const data = await response.json();
734 selectedLocation.address = data.display_name || '';
735 selectedLocation.components = data.address || {};
736 } catch (error) {
737 console.warn('Reverse geocoding failed:', error);
738 selectedLocation.address = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
739 selectedLocation.components = {};
740 }
741
742 // Update instance
743 if (window.mapPickerInstance) {
744 window.mapPickerInstance.selectedLocation = selectedLocation;
745 window.mapPickerInstance.marker = marker;
746 }
747 }
748};
749
750// Function to locate the user when requested
751window.locateUser = function() {
752 const statusDiv = document.getElementById('location-status');
753 const locateBtn = document.getElementById('locate-me-btn');
754
755 if (!window.mapPickerInstance || !window.mapPickerInstance.map) {
756 return;
757 }
758
759 const { map, translations } = window.mapPickerInstance;
760
761 // Show loading state
762 locateBtn.disabled = true;
763 locateBtn.innerHTML = `
764 <span class="icon">
765 <i class="fas fa-spinner fa-spin"></i>
766 </span>
767 <span>${translations.locatingUserText}</span>
768 `;
769 statusDiv.style.display = 'block';
770 statusDiv.textContent = translations.locatingUserText;
771 statusDiv.className = 'help has-text-info';
772
773 // Check if geolocation is supported
774 if (!navigator.geolocation) {
775 showLocationError(translations.locationErrorText);
776 return;
777 }
778
779 // Get user's location
780 navigator.geolocation.getCurrentPosition(
781 // Success callback
782 async (position) => {
783 const { latitude, longitude } = position.coords;
784
785 // Center map on user's location
786 map.flyTo({
787 center: [longitude, latitude],
788 zoom: 15,
789 duration: 2000
790 });
791
792 // Select this location
793 await window.mapPickerInstance.selectLocation(longitude, latitude);
794
795 // Reset button state
796 resetLocateButton();
797 statusDiv.style.display = 'none';
798 },
799 // Error callback
800 (error) => {
801 let errorMessage;
802 switch (error.code) {
803 case error.PERMISSION_DENIED:
804 errorMessage = translations.locationPermissionDeniedText;
805 break;
806 case error.POSITION_UNAVAILABLE:
807 case error.TIMEOUT:
808 default:
809 errorMessage = translations.locationErrorText;
810 break;
811 }
812 showLocationError(errorMessage);
813 },
814 // Options
815 {
816 enableHighAccuracy: true,
817 timeout: 10000,
818 maximumAge: 300000 // 5 minutes
819 }
820 );
821
822 function showLocationError(message) {
823 statusDiv.style.display = 'block';
824 statusDiv.textContent = message;
825 statusDiv.className = 'help has-text-danger';
826 resetLocateButton();
827 }
828
829 function resetLocateButton() {
830 const locateMeText = document.body.dataset.i18nLocateMe || 'Locate Me';
831 locateBtn.disabled = false;
832 locateBtn.innerHTML = `
833 <span class="icon">
834 <i class="fas fa-location-arrow"></i>
835 </span>
836 <span>${locateMeText}</span>
837 `;
838 }
839};
840
841window.closeMapPicker = function() {
842 const modal = document.getElementById('map-picker-modal');
843 if (modal) {
844 // Clean up map instance
845 if (window.mapPickerInstance && window.mapPickerInstance.map) {
846 window.mapPickerInstance.map.remove();
847 }
848 if (window.mapPickerInstance && window.mapPickerInstance.marker) {
849 window.mapPickerInstance.marker.remove();
850 }
851 window.mapPickerInstance = null;
852
853 modal.remove();
854 }
855};
856
857window.useSelectedLocation = function() {
858 if (window.mapPickerInstance && window.mapPickerInstance.selectedLocation) {
859 const location = window.mapPickerInstance.selectedLocation;
860
861 // Create form data for HTMX request
862 const formData = new FormData();
863 formData.append('build_state', 'Selected');
864
865 // Add location data
866 formData.append('latitude', location.lat.toString());
867 formData.append('longitude', location.lng.toString());
868
869 // Parse address components from reverse geocoding
870 if (location.address) {
871 formData.append('location_name', location.address.split(',')[0] || '');
872 }
873
874 if (location.components) {
875 // Map Nominatim address components to form fields
876 if (location.components.house_number && location.components.road) {
877 formData.append('location_street', `${location.components.house_number} ${location.components.road}`);
878 } else if (location.components.road) {
879 formData.append('location_street', location.components.road);
880 }
881
882 if (location.components.city || location.components.town || location.components.village) {
883 formData.append('location_locality', location.components.city || location.components.town || location.components.village);
884 }
885
886 if (location.components.state || location.components.province) {
887 formData.append('location_region', location.components.state || location.components.province);
888 }
889
890 if (location.components.postcode) {
891 formData.append('location_postal_code', location.components.postcode);
892 }
893
894 if (location.components.country) {
895 formData.append('location_country', location.components.country);
896 }
897 }
898
899 // Trigger HTMX request to update the form
900 if (typeof htmx !== 'undefined') {
901 htmx.ajax('POST', '/event/location', {
902 values: Object.fromEntries(formData),
903 target: '#locationGroup',
904 swap: 'outerHTML'
905 });
906 } else {
907 // Fallback: try to fill form fields directly
908 fillFormFields(location);
909 }
910
911 // Close modal
912 closeMapPicker();
913 }
914};
915
916// Fallback function to fill form fields directly
917function fillFormFields(location) {
918 const latField = document.querySelector('input[name="latitude"]');
919 const lngField = document.querySelector('input[name="longitude"]');
920 const nameField = document.querySelector('input[name="location_name"]');
921 const streetField = document.querySelector('input[name="location_street"]');
922 const localityField = document.querySelector('input[name="location_locality"]');
923 const regionField = document.querySelector('input[name="location_region"]');
924 const postalField = document.querySelector('input[name="location_postal_code"]');
925 const countryField = document.querySelector('select[name="location_country"]');
926
927 if (latField) latField.value = location.lat.toString();
928 if (lngField) lngField.value = location.lng.toString();
929
930 if (location.address && nameField) {
931 nameField.value = location.address.split(',')[0] || '';
932 }
933
934 if (location.components) {
935 if (streetField && location.components.road) {
936 if (location.components.house_number) {
937 streetField.value = `${location.components.house_number} ${location.components.road}`;
938 } else {
939 streetField.value = location.components.road;
940 }
941 }
942
943 if (localityField && (location.components.city || location.components.town || location.components.village)) {
944 localityField.value = location.components.city || location.components.town || location.components.village;
945 }
946
947 if (regionField && (location.components.state || location.components.province)) {
948 regionField.value = location.components.state || location.components.province;
949 }
950
951 if (postalField && location.components.postcode) {
952 postalField.value = location.components.postcode;
953 }
954
955 if (countryField && location.components.country) {
956 countryField.value = location.components.country;
957 }
958 }
959
960 // Trigger events to notify form of changes
961 [latField, lngField, nameField, streetField, localityField, regionField, postalField, countryField].forEach(field => {
962 if (field) {
963 field.dispatchEvent(new Event('input', { bubbles: true }));
964 field.dispatchEvent(new Event('change', { bubbles: true }));
965 }
966 });
967}
968
969// Initialize maps when DOM is ready
970document.addEventListener('DOMContentLoaded', function() {
971 // Initialize main event location map
972 const eventMapContainer = document.getElementById('event-location-map');
973 if (eventMapContainer && !eventMapContainer.hasAttribute('data-map-initialized')) {
974 eventMapContainer.setAttribute('data-map-initialized', 'true');
975 new LocationMapViewer('event-location-map');
976 }
977
978 // Initialize mini maps for venue previews
979 const miniMaps = document.querySelectorAll('.venue-map-preview, .venue-mini-map');
980 miniMaps.forEach(container => {
981 if (container.dataset.lat && container.dataset.lng && !container.hasAttribute('data-map-initialized')) {
982 container.setAttribute('data-map-initialized', 'true');
983 new MiniMapViewer(container, { height: 120 });
984 }
985 });
986});
987
988// Re-initialize after HTMX content swaps
989function setupHTMXListeners() {
990 if (document.body) {
991 document.body.addEventListener('htmx:afterSwap', function(e) {
992 // Re-initialize main event location map (check both in swapped content and globally)
993 let eventMapContainer = e.target.querySelector('#event-location-map');
994 if (!eventMapContainer) {
995 eventMapContainer = document.getElementById('event-location-map');
996 }
997 if (eventMapContainer && !eventMapContainer.hasAttribute('data-map-initialized')) {
998 eventMapContainer.setAttribute('data-map-initialized', 'true');
999 new LocationMapViewer('event-location-map');
1000 }
1001
1002 // Re-initialize mini maps in the swapped content
1003 const miniMaps = e.target.querySelectorAll('.venue-map-preview, .venue-mini-map');
1004 miniMaps.forEach(container => {
1005 if (container.dataset.lat && container.dataset.lng && !container.hasAttribute('data-map-initialized')) {
1006 container.setAttribute('data-map-initialized', 'true');
1007 new MiniMapViewer(container, { height: 120 });
1008 }
1009 });
1010 });
1011 }
1012}
1013
1014// Initialize when DOM is ready
1015if (document.readyState === 'loading') {
1016 document.addEventListener('DOMContentLoaded', setupHTMXListeners);
1017} else {
1018 setupHTMXListeners();
1019}
1020
1021// Global functions for compatibility
1022window.LocationMapViewer = LocationMapViewer;
1023window.MiniMapViewer = MiniMapViewer;