1/** 2 * @package plg_fields_aggpxtrack 3 * @copyright Copyright (C) 2005 - 2026 Astrid Günther. All rights reserved. 4 * @license GNU General Public License version 2 or later; 5 */ 6 7// Funktion zum Initialisieren der Karten 8function initAggpxMaps() { 9 10 const leafletmaps = document.querySelectorAll('.leafletmap'); 11 12 13 if (typeof L === 'undefined') { 14 console.error('AGGPXTrack Error: Leaflet (L) is not defined. Please check if leaflet.js is loaded.'); 15 return; 16 } 17 18 leafletmaps.forEach(function (container) { 19 const unique = container.getAttribute('data-unique'); 20 21 22 if (!unique) { 23 24 return; 25 } 26 27 // Optionen aus Joomla.getOptions laden 28 const options = Joomla.getOptions('plg_fields_aggpxtrack.' + unique, {}); 29 30 31 // Prüfen, ob der Container eine Höhe hat 32 if (container.clientHeight === 0) { 33 34 } 35 36 37 38 // 1. Map Initialisierung 39 const scrollWheelZoomEnabled = options.scrollwheelzoom === 1; 40 const map = L.map(container, { 41 scrollWheelZoom: scrollWheelZoomEnabled 42 }).setView([51.505, -0.09], 10); 43 44 // 2. Base Layer Auswahl 45 let tileLayer; 46 const mapType = options.maptype; 47 const osmAttrib = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'; 48 49 switch (mapType) { 50 case 'osmde': 51 tileLayer = L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { attribution: osmAttrib }); 52 break; 53 case 'opentopomap': 54 tileLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { 55 attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap', 56 maxZoom: 17 57 }); 58 break; 59 case 'thunderforest': 60 tileLayer = L.tileLayer('https://{s}.tile.thunderforest.com/' + options.thunderforestmaptype + '/{z}/{x}/{y}.png?apikey=' + options.thunderforestkey, { 61 attribution: '© Thunderforest, © OpenStreetMap contributors', 62 maxZoom: 22 63 }); 64 break; 65 case 'mapbox': 66 tileLayer = L.tileLayer('https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}
66?access_token=' + options.mapboxkey, { 67 attribution: '© Mapbox, © OpenStreetMap contributors', 68 tileSize: 512, 69 zoomOffset: -1 70 }); 71 break; 72 case 'geoportailfrance': 73 tileLayer = L.tileLayer('https://wxs.ign.fr/' + options.geoportailfrancekey + '/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE=normal&TILEMATRIXSET=PM&FORMAT=image/jpeg&LAYER=GEOGRAPHICALGRIDSYSTEMS.MAPS&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', { 74 attribution: '© IGN-F/Geoportail', 75 maxZoom: 18 76 }); 77 break; 78 case 'google': 79 if (L.gridLayer.googleMutant) { 80 tileLayer = L.gridLayer.googleMutant({ 81 type: options.googlemapstype || 'roadmap' 82 }); 83 } 84 break; 85 default: 86 tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: osmAttrib }); 87 } 88 tileLayer.addTo(map); 89 90 // Karte am Container speichern, damit andere Skripte darauf zugreifen können 91 container.leaflet_map = map; 92 93 if (typeof L.GPX === 'undefined') { 94 console.error('AGGPXTrack Error: Leaflet.GPX plugin is not loaded.'); 95 return; 96 } 97 98 // 3. GPX Konfiguration 99 // showelements Parameter verarbeiten (z.B. "track,waypoint" -> ["track", "waypoint"]) 100 let parseElements = ['track', 'route', 'waypoint']; // Standard 101 if (options.showelements) { 102 parseElements = options.showelements.split(',').map(s => s.trim()); 103 } 104 105 // Marker Options: Benutzerdefinierte Icons oder Standard 106 let markerOptions = {}; 107 108 if (options.custom_icons === 1 && typeof L.AwesomeMarkers !== 'undefined') { 109 110 111 const wptIcon = L.AwesomeMarkers.icon({ 112 icon: options.customiconwaypoint_icon || 'home', 113 prefix: 'fa', 114 markerColor: options.customiconwaypoint_markercolor || 'red', 115 iconColor: options.customiconwaypoint_iconcolor || '#ffffff', 116 spin: options.customiconwaypoint_spin === 'true' || options.customiconwaypoint_spin === '1', 117 extraClasses: options.customiconwaypoint_extraclasses || '' 118 }); 119 120 121 // Verwende benutzerdefinierte Font Awesome Icons 122 markerOptions = { 123 startIcon: L.AwesomeMarkers.icon({ 124 icon: options.customiconstart_icon || 'home', 125 prefix: 'fa', 126 markerColor: options.customiconstart_markercolor || 'red', 127 iconColor: options.customiconstart_iconcolor || '#ffffff', 128 spin: options.customiconstart_spin === 'true' || options.customiconstart_spin === '1', 129 extraClasses: options.customiconstart_extraclasses || '' 130 }), 131 endIcon: L.AwesomeMarkers.icon({ 132 icon: options.customiconend_icon || 'home', 133 prefix: 'fa', 134 markerColor: options.customiconend_markercolor || 'red', 135 iconColor: options.customiconend_iconcolor || '#ffffff', 136 spin: options.customiconend_spin === 'true' || options.customiconend_spin === '1', 137 extraClasses: options.customiconend_extraclasses || '' 138 }), 139 wptIcons: { 140 '': wptIcon 141 }, 142 clickable: true 143 }; 144 145 146 // Special Waypoints hinzufügen 147 if (options.specialwaypoints && Array.isArray(options.specialwaypoints)) { 148 149 options.specialwaypoints.forEach(function(wp) { 150 if (wp.tagname) { 151 markerOptions.wptIcons[wp.tagname] = L.AwesomeMarkers.icon({ 152 icon: wp.icon || 'home', 153 prefix: 'fa', 154 markerColor: wp.markercolor || 'red', 155 iconColor: wp.iconcolor || '#ffffff', 156 spin: wp.spin === '1',
157 extraClasses: wp.extraclasses || '' 158 }); 159 } 160 }); 161 } 162 163 markerOptions.wptIconTypeUrls = {}; 164 markerOptions.wptIconUrls = {}; 165 } else { 166 167 // Verwende Standard PNG Icons 168 markerOptions = { 169 startIconUrl: options.startIconUrl, 170 endIconUrl: options.endIconUrl, 171 shadowUrl: options.shadowUrl, 172 wptIconUrls: { '': options.wptIconUrls }, 173 wptIconTypeUrls: options.wptIconTypeUrls || {}, 174 clickable: true 175 }; 176 } 177 178 const gpxLayer = new L.GPX(options.gpx_file_name, { 179 async: true, 180 gpx_options: { 181 parseElements: parseElements, 182 show_kilometer_point: options.show_kilometer_points === 1, 183 kilometer_point_options: { 184 kilometer_point_color: options.kilometer_point_color || '#0000ff', 185 kilometer_point_color_text: options.kilometer_point_color_text || '#ffffff', 186 kilometer_point_intervall: parseInt(options.kilometer_point_intervall) || 1, 187 kilometer_point_radius: parseInt(options.kilometer_point_radius) || 10 188 }, 189 joinTrackSegments: true 190 }, 191 polyline_options: { 192 color: options.polylinecolor || '#0000ff', 193 opacity: 0.75, 194 weight: 5, 195 lineCap: 'round' 196 }, 197 marker_options: markerOptions 198 }); 199 200 // 4. Elevation Control (Höhenprofil) 201 let elControl = null; 202 if (options.show_elevationcontrol === 1) { 203 // Funktion zum Initialisieren des Elevation Controls 204 const initElevationControl = function() { 205 if (typeof d3 === 'undefined') { 206 console.error('AGGPXTrack: D3 library not loaded'); 207 return; 208 } 209 if (typeof L.Control.Elevation === 'undefined' || typeof L.control.elevation !== 'function') { 210 211 setTimeout(initElevationControl, 100); 212 return; 213 } 214 215 216 217 218 219 220 const elevationPosition = options.elevationcontrol_position || 'topright'; 221 const isCollapsed = options.elevationcontrol_collapsed === 1; 222 const elevationTheme = options.elevationcontrol_theme || 'lime-theme'; 223 224 const elevationOptions = { 225 theme: elevationTheme, 226 width: parseInt(options.elevationcontrol_width) || 600, 227 height: parseInt(options.elevationcontrol_height) || 170, 228 collapsed: isCollapsed, 229 summary: options.elevationcontrol_summary || 'multiline', 230 margins: { 231 top: parseInt(options.elevationcontrol_margins_top) || 10, 232 right: parseInt(options.elevationcontrol_margins_right) || 20, 233 bottom: parseInt(options.elevationcontrol_margins_bottom) || 30, 234 left: parseInt(options.elevationcontrol_margins_left) || 50 235 } 236 }; 237 238 // Position nur setzen wenn es eine gültige Leaflet-Position ist 239 if (['topleft', 'topright', 'bottomleft', 'bottomright'].includes(elevationPosition)) { 240 elevationOptions.position = elevationPosition; 241 } else { 242 elevationOptions.position = 'topright'; 243 } 244 245 246 elControl = L.control.elevation(elevationOptions); 247 248 249 250 // Wenn Position 'above' oder 'below', manuell in DIV rendern 251 if (elevationPosition === 'above' || elevationPosition === 'below') { 252 const targetDiv = elevationPosition === 'above' 253 ? document.getElementById('aggpxtrackabove' + unique) 254 : document.getElementById('aggpxtrackbelow' + unique); 255 256 if (targetDiv) { 257
258 elControl.onAdd(map); 259 targetDiv.appendChild(elControl.getContainer()); 260 261 // Button muss auch hier sichtbar gemacht werden 262 if (isCollapsed && elControl._button) { 263 elControl._button.style.display = 'block'; 264 elControl._button.style.position = 'absolute'; 265 elControl._button.style.top = '0'; 266 elControl._button.style.left = '0'; 267 elControl._button.style.zIndex = '10000'; 268 } 269 } else { 270 console.error('AGGPXTrack: Target div not found for position', elevationPosition); 271 } 272 } else { 273 elControl.addTo(map); 274 275 276 277 // Für topleft: Button muss über anderen Controls sein 278 if (elevationPosition === 'topleft' && isCollapsed && elControl._button) { 279 elControl._button.style.zIndex = '10000'; 280 } 281 } 282 283 284 285 // Fix für Toggle-Button: Immer hinzufügen, auch wenn nicht collapsed 286 // Damit alte Felder ohne elevationcontrol_collapsed auch funktionieren 287 if (elControl._button) { 288 289 290 // Mache Button sichtbar per JavaScript 291 elControl._button.style.display = 'block'; 292 elControl._button.style.width = '36px'; 293 elControl._button.style.height = '36px'; 294 elControl._button.style.visibility = 'visible'; 295 elControl._button.style.opacity = '1'; 296 elControl._button.style.position = 'relative'; 297 elControl._button.style.zIndex = '1000'; 298 elControl._button.style.backgroundColor = '#f8f8f9'; 299 elControl._button.style.border = '2px solid #333'; 300 elControl._button.style.borderRadius = '5px'; 301 elControl._button.style.cursor = 'pointer'; 302 303 // Füge Text hinzu damit Button erkennbar ist 304 elControl._button.innerHTML = 'â¼'; 305 elControl._button.style.fontSize = '20px'; 306 elControl._button.style.lineHeight = '32px'; 307 elControl._button.style.textAlign = 'center'; 308 elControl._button.style.color = '#333'; 309 elControl._button.title = 'Höhenprofil ein-/ausblenden'; 310 311 // Entferne alle existierenden Handler 312 L.DomEvent.off(elControl._button, 'click'); 313 L.DomEvent.off(elControl._button, 'mousedown'); 314 L.DomEvent.off(elControl._button, 'touchstart'); 315 316 // Füge Handler mit nativem addEventListener hinzu (höhere Priorität) 317 const toggleHandler = function(e) { 318 e.preventDefault(); 319 e.stopPropagation(); 320 e.stopImmediatePropagation(); 321 322 const container = elControl._container; 323 const svg = container.querySelector('svg'); 324 325 if (container.classList.contains('elevation-collapsed')) { 326 container.classList.remove('elevation-collapsed'); 327 if (svg) svg.style.display = 'block'; 328 329 } else { 330 container.classList.add('elevation-collapsed'); 331 if (svg) svg.style.display = 'none'; 332 333 } 334 return false; 335 }; 336 337 // Verwende nur click event, nicht mousedown 338 elControl._button.addEventListener('click', toggleHandler, true); 339 340 // Initiales Verstecken des SVG wenn collapsed 341 if (isCollapsed) { 342 const svg = elControl._container.querySelector('svg'); 343 if (svg) svg.style.display = 'none'; 344 } 345 } 346 347 gpxLayer.on('addline', function(e) { 348 349 elControl.addData(e.line); 350 351 // Fallback: Setze Farbe direkt per JavaScript 352 setTimeout(function() { 353 const areaPath = document.querySelector('.elevation.' + elevationTheme + ' path.area'); 354 if (areaPath) {
355 const themeColors = { 356 'lime-theme': '#9cc222', 357 'steelblue-theme': '#4682b4', 358 'purple-theme': '#732c7b' 359 }; 360 const color = themeColors[elevationTheme] || '#9cc222'; 361 areaPath.style.fill = color; 362 363 } 364 }, 100); 365 }); 366 }; 367 368 // Starte Initialisierung 369 initElevationControl(); 370 } 371 372 // 5. Scale & Fullscreen 373 if (options.show_fullscreencontrol === 1 && L.Control.Fullscreen) { 374 map.addControl(new L.Control.Fullscreen()); 375 } 376 377 if (options.scale !== 0) { 378 L.control.scale({ 379 metric: options['scale-metric'] === 1, 380 imperial: options['scale-imperial'] === 1 381 }).addTo(map); 382 } 383 384 // 6. Locate Control (Aktuelle Position) 385 if (options.show_currentposition === 1 && L.control.locate) { 386 L.control.locate({ 387 position: options.currentposition_position || 'topleft', 388 strings: { 389 title: "Show me where I am" 390 }, 391 locateOptions: { 392 enableHighAccuracy: true 393 } 394 }).addTo(map); 395 } 396 397 // 7. EasyPrint 398 if (options.show_easyprint === 1 && L.easyPrint) { 399 L.easyPrint({ 400 position: options.easyprint_position || 'topleft', 401 sizeModes: ['Current', 'A4Landscape', 'A4Portrait'], 402 filename: 'map-export', 403 exportOnly: true, 404 hideControlContainer: true 405 }).addTo(map); 406 } 407 408 // 8. Omnivore (Zusätzliche Layer) 409 if (options.show_omnivore === 1 && typeof omnivore !== 'undefined') { 410 const omnivoreFile = options.omnivore_file; 411 if (omnivoreFile) { 412 // Erkennt Format automatisch anhand der Endung (kml, gpx, csv, etc.) 413 let layer; 414 const ext = omnivoreFile.split('.').pop().toLowerCase(); 415 416 if (ext === 'gpx') { 417 layer = omnivore.gpx(omnivoreFile); 418 } else if (ext === 'csv' || ext === 'tsv') { 419 layer = omnivore.csv(omnivoreFile); 420 } else { 421 // Default zu KML 422 layer = omnivore.kml(omnivoreFile); 423 } 424 425 if (layer) { 426 // Binde Popups für jeden Layer 427 layer.on('ready', function() { 428 layer.eachLayer(function(marker) { 429 if (marker.feature && marker.feature.properties) { 430 const props = marker.feature.properties; 431 let popupContent = ''; 432 433 if (props.name) { 434 popupContent += '<strong>' + props.name + '</strong><br>'; 435 } 436 if (props.description) { 437 popupContent += props.description; 438 } 439 440 if (popupContent) { 441 marker.bindPopup(popupContent); 442 } 443 } 444 }); 445 }); 446 447 layer.addTo(map); 448 } 449 } 450 } 451 452 // 9. GPX Loaded: Zoom & Statistiken füllen 453 gpxLayer.on('loaded', function (e) { 454 const gpx = e.target; 455 map.fitBounds(gpx.getBounds(), { padding: [20, 20] }); 456 457 // Statistiken aus dem GPX-Objekt auslesen 458 const stats = { 459 'distance': (gpx.get_distance() / 1000).toFixed(2), 460 'eleMax': gpx.get_elevation_max().toFixed(0), 461 'eleMin': gpx.get_elevation_min().toFixed(0), 462 'ele': gpx.get_elevation_gain().toFixed(0), // Mapping für 'ele' im PHP Template 463 'eleGain': gpx.get_elevation_gain().toFixed(0), 464 'eleLoss': gpx.get_elevation_loss().toFixed(0), 465 'speed': gpx.get_moving_speed().toFixed(1), 466 'hr': (gpx.get_heartrate_avg && gpx.get_heartrate_avg()
466) ? gpx.get_heartrate_avg().toFixed(0) : '-', 467 'cad': (gpx.get_cadence_avg && gpx.get_cadence_avg()) ? gpx.get_cadence_avg().toFixed(0) : '-', 468 'atemp': (gpx.get_temp_avg && gpx.get_temp_avg()) ? gpx.get_temp_avg().toFixed(1) : '-' 469 }; 470 471 // HTML Elemente mit den berechneten Werten füllen 472 Object.keys(stats).forEach(key => { 473 const valEl = document.getElementById(key + '_value' + unique); 474 if (valEl) valEl.innerHTML = stats[key]; 475 }); 476 }); 477 478 gpxLayer.on('error', function(e) { 479 console.error('AGGPXTrack GPX Load Error:', e); 480 }); 481 482 gpxLayer.addTo(map); 483 484 // Event feuern, damit externe Skripte (markers, panellayers, layertree) wissen, dass die Karte bereit ist. 485 const event = new CustomEvent('aggpxtrack:ready', { 486 detail: { 487 map: map, 488 unique: unique, 489 options: options 490 } 491 }); 492 container.dispatchEvent(event); 493 }); 494} 495 496// Warte bis alle Scripts geladen sind (window.load statt DOMContentLoaded) 497if (document.readyState === 'complete') { 498 // Seite bereits geladen 499 initAggpxMaps(); 500} else { 501 // Warte auf window.load (alle Scripts inkl. defer sind geladen) 502 window.addEventListener('load', initAggpxMaps); 503}
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.