Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
at main 40 kB view raw
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;