Przejdź do głównej treści

GPX OpenStreetMap Viewer

Grafika SVG Lokalizacja

Prosta, lekka aplikacja webowa pozwalająca wczytywać i wyświetlać trasy GPX bez instalowania czegokolwiek.
Działa w całości w przeglądarce – wystarczy wczytać plik GPX i już możesz przeglądać swoje trasy!

Co potrafi

  • Wczytuje pliki GPX z dysku lub z adresu URL
  • Wyświetla trasę na mapie OpenStreetMap (Leaflet)
  • Automatycznie dopasowuje widok mapy do trasy
  • Oblicza długość trasy (w kilometrach)
  • Obsługuje wiele tras naraz — każda w innym kolorze
  • Możliwość ukrycia, pokazania lub usunięcia trasy z mapy

Jak używać

  1. Wczytaj plik .gpx z dysku lub wklej link do pliku z sieci.
  2. Gotowe! Trasa pojawi się na mapie — możesz:
    • przybliżać widok,
    • wyświetlać wiele tras naraz,
    • usuwać je z listy jednym kliknięciem.

Technologie

  • HTML5 + Bootstrap 5.3 – nowoczesny, responsywny interfejs
  • Leaflet – mapa bazująca na danych OpenStreetMap
  • toGeoJSON – konwersja plików GPX (XML) na GeoJSON

Dlaczego warto

  • Nie wymaga logowania ani serwera
  • Idealne narzędzie dla rowerzystów, biegaczy i organizatorów rajdów
  • Możesz analizować swoje trasy przeglądając mapę

Sprawdź, jak wygląda Twoja trasa z perspektywy mapy!
Wczytaj plik GPX i zobacz, dokąd prowadzi Twój szlak.

Wczytaj plik(i) GPX

Możesz dodać kilka plików naraz.


lub URL do pliku GPX

Lista tras
Brak wczytanych tras
Uwaga: wczytywanie GPX z zewnętrznych serwerów może być blokowane przez CORS.
Profil wysokości
Podsumowanie trasy
Nazwa trasy Długość [km] Przewyższenie + [m] Średnie nachylenie [%]

Kod po stronie przeglądarki

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="" crossorigin=""/>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css" />
<style>
#map { height: 70vh; min-height: 400px; }
.track-item { cursor: pointer; }
[data-bs-theme=golden] .btn-outline-danger {
  --bs-btn-color: #d13241;
  --bs-btn-border-color: #d13241;
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #d13241;
  --bs-btn-hover-border-color: #d13241;
  --bs-btn-focus-shadow-rgb: 220, 53, 69;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #d13241;
  --bs-btn-active-border-color: #d13241;
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #d13241;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #d13241;
  --bs-gradient: none;
}
[data-bs-theme=twilight] .btn-outline-danger {
  --bs-btn-color: #ff4c65;
  --bs-btn-border-color: #ff4c65;
  --bs-btn-hover-color: #000;
  --bs-btn-hover-bg: #ff4c65;
  --bs-btn-hover-border-color: #ff4c65;
  --bs-btn-focus-shadow-rgb: 220, 53, 69;
  --bs-btn-active-color: #000;
  --bs-btn-active-bg: #ff4c65;
  --bs-btn-active-border-color: #ff4c65;
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #ff4c65;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #ff4c65;
  --bs-gradient: none;
}
[data-bs-theme=dark] .btn-outline-danger {
  --bs-btn-color: #ff4359;
  --bs-btn-border-color: #ff4359;
  --bs-btn-hover-color: #000;
  --bs-btn-hover-bg: #ff4359;
  --bs-btn-hover-border-color: #ff4359;
  --bs-btn-focus-shadow-rgb: 220, 53, 69;
  --bs-btn-active-color: #000;
  --bs-btn-active-bg: #ff4359;
  --bs-btn-active-border-color: #ff4359;
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #ff4359;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #ff4359;
  --bs-gradient: none;
}   
[data-bs-theme=twilight] .btn-outline-secondary {
  --bs-btn-color: #8d9aa4;
  --bs-btn-border-color: #8d9aa4;
  --bs-btn-hover-color: #000;
  --bs-btn-hover-bg: #8d9aa4;
  --bs-btn-hover-border-color: #8d9aa4;
  --bs-btn-focus-shadow-rgb: 108, 117, 125;
  --bs-btn-active-color: #000;
  --bs-btn-active-bg: #8d9aa4 ;
  --bs-btn-active-border-color: #8d9aa4;
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #8d9aa4;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #8d9aa4;
  --bs-gradient: none;
}	
[data-bs-theme=dark] .btn-outline-secondary {
  --bs-btn-color: #87939d;
  --bs-btn-border-color: #87939d;
  --bs-btn-hover-color: #000;
  --bs-btn-hover-bg: #87939d;
  --bs-btn-hover-border-color: #87939d;
  --bs-btn-focus-shadow-rgb: 108, 117, 125;
  --bs-btn-active-color: #000;
  --bs-btn-active-bg: #87939d ;
  --bs-btn-active-border-color: #87939d;
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #87939d;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #87939d;
  --bs-gradient: none;
}	        
</style>
<div id="app">    
    <div class="row g-3 mb-3">
      <div class="col-md-5">
        <div class="card">
          <div class="card-body">
            <h3 class="card-title">Wczytaj plik(i) GPX</h3>
            <p class="card-text small text-muted">Możesz dodać kilka plików naraz.</p>
            <input id="gpxFiles" class="form-control mb-2" type="file" accept=".gpx,application/gpx+xml" multiple aria-label="Wybierz pliki">
            <hr>
            <h4 class="h6">lub URL do pliku GPX</h4>
            <div class="input-group mb-2">
              <input id="gpxUrl" type="url" class="form-control" placeholder="https://example.com/route.gpx" aria-label="Wczytaj adres URL">
              <button id="loadUrlBtn" class="btn btn-primary">Wczytaj</button>
            </div>
            <div class="form-check form-switch mb-2">
              <input class="form-check-input" type="checkbox" id="autoFit" checked>
              <label class="form-check-label" for="autoFit">Automatycznie dopasuj mapę po załadowaniu</label>
            </div>
            <div id="alerts"></div>
          </div>
        </div>

        <div class="card mt-3">
          <div class="card-body">
            <h5 class="card-title">Lista tras</h5>
            <div id="tracksList" class="list-group">
              <div class="list-group-item small text-muted">Brak wczytanych tras</div>
            </div>
            <div class="mt-2">
              <button id="clearAll" class="btn btn-outline-danger btn-sm">Usuń wszystkie</button>
            </div>
          </div>
        </div>
      </div>

      <div class="col-md-7">
        <div id="map" class="border rounded"></div>        
        <div class="mt-2 small text-muted">Uwaga: wczytywanie GPX z zewnętrznych serwerów może być blokowane przez CORS.</div>
        <!-- --- PROFIL WYSOKOŚCI --- -->
        <div class="card mt-3">
          <div class="card-body">
            <h5 class="card-title">Profil wysokości</h5>
            <canvas id="elevationChart" height="150"></canvas>
          </div>
        </div>
        <!-- --- KONIEC --- -->
      </div>
      
    </div>   
    
    <div class="card mt-4">
      <div class="card-header">
        <strong><i class="bi bi-bar-chart"></i> Podsumowanie trasy</strong>
      </div>
      <div class="card-body p-0">
        <table class="table table-striped mb-0" id="summaryTable">
          <thead>
            <tr>
              <th>Nazwa trasy</th>
              <th>Długość [km]</th>
              <th>Przewyższenie + [m]</th>
              <th>Średnie nachylenie [%]</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>

</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/togeojson/0.16.0/togeojson.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
  // ---------- Inicjalizacja mapy ----------
  const map = L.map('map', { preferCanvas: true }).setView([52.2297, 21.0122], 13); // domyślnie Warszawa
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; OpenStreetMap contributors'
  }).addTo(map);

  // Kontenery
  const tracks = []; // { id, name, layer, color, lengthKm, bounds }
  const colors = [
    '#0d6efd', // primary
    '#6c757d', // secondary
    '#198754', // success
    '#dc3545', // danger
    '#ffc107', // warning
    '#0dcaf0', // info
    '#6610f2', // purple (Bootstrap accent)
    '#fd7e14', // orange
    '#20c997', // teal
    '#212529'  // dark
  ];

  // Elementy UI
  const gpxFilesInput = document.getElementById('gpxFiles');
  const tracksListEl = document.getElementById('tracksList');
  const alertsEl = document.getElementById('alerts');
  const loadUrlBtn = document.getElementById('loadUrlBtn');
  const gpxUrlInput = document.getElementById('gpxUrl');
  const autoFitCheckbox = document.getElementById('autoFit');
  const clearAllBtn = document.getElementById('clearAll');

  // --- Chart.js setup ---
  let chart = new Chart(document.getElementById('elevationChart'), {
    type: 'line',
    data: { labels: [], datasets: [{
      label: 'Wysokość (m)',
      data: [],
      borderColor: '#007bff',
      fill: true,
      tension: 0.1,
      pointRadius: 0
    }]},
    options: {
      scales: {
        x: { title: { display: true, text: 'Dystans (km)' }},
        y: { title: { display: true, text: 'Wysokość (m)' }}
      },
      plugins: { legend: { display: false } },
      responsive: true,
      maintainAspectRatio: true
    }
  });

  // ---------- Pomocnicze: komunikaty ----------
  function showAlert(msg, type='info', timeout=5000) {
    const id = 'a' + Date.now();
    const wrapper = document.createElement('div');
    wrapper.innerHTML = `
      <div id="${id}" class="alert alert-${type} alert-dismissible fade show" role="alert">
        ${escapeHtml(msg)}
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
      </div>`;
    alertsEl.appendChild(wrapper);
    if (timeout > 0) setTimeout(()=> {
      const el = document.getElementById(id);
      if (el) el.remove();
    }, timeout);
  }
  function escapeHtml(unsafe) {
    return unsafe
      .replaceAll('&','&amp;')
      .replaceAll('<','&lt;')
      .replaceAll('>','&gt;')
      .replaceAll('"','&quot;')
      .replaceAll("'",'&#039;');
  }

  // ---------- Parsowanie GPX (z File lub z tekstu) ----------
  async function parseGpxText(gpxText) {
    try {
      const parser = new DOMParser();
      const xml = parser.parseFromString(gpxText, "application/xml");
      // check for parsererror
      if (xml.querySelector('parsererror')) {
        throw new Error('Błąd parsowania GPX (XML) – prawdopodobnie uszkodzony plik.');
      }
      // konwersja do GeoJSON (togeojson)
      const geojson = toGeoJSON.gpx(xml);
      return { xml, geojson };
    } catch (err) {
      throw err;
    }
  }

  // --- Odczyt profilu wysokości z <trkpt> ---
  function extractElevationProfile(xml){
    const pts = Array.from(xml.querySelectorAll('trkpt'));
    const data = [];
    let dist = 0;
    let prev = null;
    for(const p of pts){
      const lat = parseFloat(p.getAttribute('lat'));
      const lon = parseFloat(p.getAttribute('lon'));
      const ele = parseFloat(p.querySelector('ele')?.textContent || 0);
      const ll = L.latLng(lat, lon);
      if(prev) dist += prev.distanceTo(ll) / 1000; // km
      data.push({ dist, ele });
      prev = ll;
    }
    return data;
  }

  function updateChart(profile, color="#0000ff"){
    if(!profile || profile.length===0){
      chart.data.labels = [];
      chart.data.datasets[0].data = [];
      chart.update();
      return;
    }    
    
    chart.data.labels = profile.map(p => p.dist.toFixed(2));
    chart.data.datasets[0].data = profile.map(p => p.ele);
    chart.data.datasets[0].borderColor = color;
    chart.update();
  }
    
// ========================
// 🔹 Pomocnicza funkcja dystansu
// ========================
function haversineDistanceKm(lat1, lon1, lat2, lon2) {
  const R = 6371;
  const toRad = Math.PI / 180;
  const dLat = (lat2 - lat1) * toRad;
  const dLon = (lon2 - lon1) * toRad;
  const a = Math.sin(dLat/2)**2 +
            Math.cos(lat1*toRad)*Math.cos(lat2*toRad) *
            Math.sin(dLon/2)**2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
    
// ========================
// 🔹 Spłaszczenie współrzędnych z GeoJSON
// ========================
function flattenCoords(coords) {
  const out = [];
  (function rec(c) {
    if (!c) return;
    if (typeof c[0] === 'number') {
      out.push(c);
    } else {
      for (const e of c) rec(e);
    }
  })(coords);
  return out;
}
    
// ========================
// 🔹 Wygładzanie wysokości (średnia ruchoma)
// ========================
function smoothElevations(elevs, window = 5) {
  if (window <= 1) return elevs.slice();
  const n = elevs.length;
  const out = new Array(n);
  const half = Math.floor(window / 2);
  for (let i = 0; i < n; i++) {
    let sum = 0, cnt = 0;
    for (let j = Math.max(0, i - half); j <= Math.min(n - 1, i + half); j++) {
      if (!isNaN(elevs[j])) { sum += elevs[j]; cnt++; }
    }
    out[i] = cnt ? sum / cnt : NaN;
  }
  return out;
}    
    
// ========================
// 📊 Główna funkcja analizująca trasę GPX/GeoJSON
// ========================
function analyzeGeoJSON(geojson, opts = {}) {
  const smoothingWindow = opts.smoothingWindow ?? 7;
  const minElevChange = opts.minElevationChange ?? 3;

  // Zbieranie współrzędnych
  let allCoords = [];
  for (const feat of (geojson.features || [])) {
    if (!feat.geometry) continue;
    const type = feat.geometry.type;
    if (type === 'LineString' || type === 'MultiLineString') {
      allCoords = allCoords.concat(flattenCoords(feat.geometry.coordinates));
    } else if (type === 'GeometryCollection') {
      for (const g of feat.geometry.geometries || []) {
        if (g.type === 'LineString' || g.type === 'MultiLineString') {
          allCoords = allCoords.concat(flattenCoords(g.coordinates));
        }
      }
    }
  }

  if (allCoords.length < 2) {
    return { distanceKm: 0, totalAscentM: 0, totalDescentM: 0, avgSlopePct: 0 };
  }

  const pts = allCoords.map(c => ({
    lon: c[0], lat: c[1], ele: c[2]
  }));

  const elevsRaw = pts.map(p => isNaN(p.ele) ? NaN : p.ele);
  const anyEle = elevsRaw.some(v => !isNaN(v));

  let totalDistance = 0;
  let ascent = 0;
  let descent = 0;

  if (!anyEle) {
    // tylko dystans
    for (let i = 1; i < pts.length; i++) {
      totalDistance += haversineDistanceKm(
        pts[i - 1].lat, pts[i - 1].lon,
        pts[i].lat, pts[i].lon
      );
    }
    return { distanceKm: totalDistance, totalAscentM: null, totalDescentM: null, avgSlopePct: null };
  }

  const elevs = smoothElevations(elevsRaw, smoothingWindow);
  let prevLat = pts[0].lat, prevLon = pts[0].lon, prevEle = elevs[0];

  for (let i = 1; i < pts.length; i++) {
    const cur = pts[i];
    const d = haversineDistanceKm(prevLat, prevLon, cur.lat, cur.lon);
    totalDistance += d;

    const curEle = elevs[i];
    if (!isNaN(curEle) && !isNaN(prevEle)) {
      const delta = curEle - prevEle;
      if (Math.abs(delta) >= minElevChange) {
        if (delta > 0) ascent += delta;
        else descent += -delta;
      }
    }
    prevLat = cur.lat; prevLon = cur.lon; prevEle = curEle;
  }

  const avgSlopePct = ascent > 0 && totalDistance > 0
    ? (ascent / (totalDistance * 1000)) * 100
    : 0;

  return {
    distanceKm: totalDistance.toFixed(2),
    totalAscentM: Math.round(ascent),
    totalDescentM: Math.round(descent),
    avgSlopePct: avgSlopePct.toFixed(2)
  };
}

    
// ========================
// 📝 Dodawanie wyników do tabeli
// ========================
function addSummaryRow(geojson) {
  const s = analyzeGeoJSON(geojson, { smoothingWindow: 7, minElevationChange: 3 });
  const name = geojson.features[0].properties.name || "Nieznana trasa";
  const tbody = document.querySelector("#summaryTable tbody");
  const row = document.createElement("tr");
  row.innerHTML = `
    <td>${name}</td>
    <td>${s.distanceKm} km</td>
    <td>${s.totalAscentM} m</td>
    <td>${s.avgSlopePct} %</td>
  `;
  tbody.appendChild(row);
}

  // ---------- Dodaj trasę na mapę ----------
  function addTrack(geojson, xml, metaName) {
    // Wybierz kolor cyklicznie
    const color = colors[tracks.length % colors.length];
    
    // Utwórz warstwę Leaflet z GeoJSON
    const trackLayer = L.geoJSON(geojson, {
      style: function(feature) {
        return { color: color, weight: 4, opacity: 0.85 };
      },
      pointToLayer: function(feature, latlng) {
        // jeżeli punkt (np. waypoint) - zrób marker
        return L.circleMarker(latlng, { radius: 4, weight:1 });
      },
      onEachFeature: function(feature, layer) {
        let html = '';
        if (feature.properties && Object.keys(feature.properties).length) {
          html += '<div>';
          for (const k in feature.properties) {
            // ograniczanie długości
            const v = String(feature.properties[k]).slice(0,200);
            html += `<strong>${escapeHtml(k)}:</strong> ${escapeHtml(v)}<br/>`;
          }
          html += '</div>';
        }
        if (html) layer.bindPopup(html);
      }
    }).addTo(map);

    // compute bounds and length
    const bounds = trackLayer.getBounds();
    const lengthKm = computeLayerLengthKm(trackLayer);

    const id = 'track-' + Date.now() + '-' + Math.floor(Math.random()*1000);
    const name = metaName || ('Trasa ' + (tracks.length + 1));
    const profile = extractElevationProfile(xml);
      
    addSummaryRow(geojson);  
    
    tracks.push({ id, name, layer: trackLayer, color, lengthKm, bounds, profile });
    refreshTracksList();
    updateChart(profile, color);
    
    if (autoFitCheckbox.checked) {
      if (bounds.isValid()) map.fitBounds(bounds.pad(0.1));
    }
    showAlert(`Dodano trasę "${name}" — długość: ${lengthKm.toFixed(2)} km`, 'success', 4000);
  }

  // ---------- Oblicz długość trasy w km (sumujemy segmenty liniowe) ----------
  function computeLayerLengthKm(layer) {
    let totalMeters = 0;
    layer.eachLayer(function(sub) {
      if (sub instanceof L.Polyline) {
        const latlngs = sub.getLatLngs();
        // Jeśli latlngs jest zagnieżdżone (multilinestring), obsłuż rekurencyjnie
        const flat = flattenLatLngs(latlngs);
        for (let i=1;i<flat.length;i++){
          totalMeters += flat[i-1].distanceTo(flat[i]); // Leaflet distanceTo (meters)
        }
      }
    });
    return totalMeters / 1000;
  }
  function flattenLatLngs(arr) {
    // flattens nested arrays of latlngs
    const out = [];
    (function f(a){
      if (!a) return;
      if (Array.isArray(a)) {
        for (const e of a) f(e);
      } else {
        out.push(a);
      }
    })(arr);
    return out;
  }

  // ---------- UI: odśwież lista tras ----------
  function refreshTracksList() {
    tracksListEl.innerHTML = '';
    if (tracks.length === 0) {
      tracksListEl.innerHTML = '<div class="list-group-item small text-muted">Brak wczytanych tras</div>';
      return;
    }
    tracks.forEach((t, idx) => {
      const item = document.createElement('div');
      item.className = 'list-group-item d-flex justify-content-between align-items-start track-item';
      item.innerHTML = `
        <div>
          <div class="fw-semibold">${escapeHtml(t.name)}</div>
          <div class="small text-muted">Długość: ${t.lengthKm.toFixed(2)} km</div>
        </div>
        <div class="text-end">
          <div class="mb-1">
            <button class="btn btn-sm btn-outline-secondary btn-zoom" data-id="${t.id}" title="Dopasuj widok"><i class="bi bi-search"></i></button>
            <button class="btn btn-sm btn-outline-secondary btn-toggle" data-id="${t.id}" title="Pokaż/ukryj"><i class="bi bi-eye"></i></button>
          </div>
          <div style="width:18px;height:8px;border-radius:2px;background:${t.color};margin:0 auto;"></div>
        </div>
      `;
      item.onclick = ()=>{ map.fitBounds(t.bounds.pad(0.1)); updateChart(t.profile, t.color); };
      tracksListEl.appendChild(item);
    });

    // Podłącz zdarzenia
    tracksListEl.querySelectorAll('.btn-zoom').forEach(btn => {
      btn.onclick = () => {
        const id = btn.dataset.id;
        const t = tracks.find(x => x.id === id);
        if (t && t.bounds.isValid()) map.fitBounds(t.bounds.pad(0.1));
      };
    });
    tracksListEl.querySelectorAll('.btn-toggle').forEach(btn => {
      btn.onclick = () => {
        const id = btn.dataset.id;
        const t = tracks.find(x => x.id === id);
        if (!t) return;
        if (map.hasLayer(t.layer)) {
          map.removeLayer(t.layer);
          btn.innerHTML = '<i class="bi bi-ban"></i>';
        } else {
          map.addLayer(t.layer);
          btn.innerHTML = '<i class="bi bi-eye"></i>';
        }
      };
    });
  }

  // ---------- Wczytanie pliku lokalnego (File API) ----------
  gpxFilesInput.addEventListener('change', async (ev) => {
    const files = Array.from(ev.target.files || []);
    if (files.length === 0) return;
    for (const f of files) {
      try {
        const text = await f.text();
        const { geojson } = await parseGpxText(text);
        // Spróbuj pobrać nazwę z pliku GPX / XML <name> w <trk> albo <metadata><name>
        const parser = new DOMParser();
        const xml = parser.parseFromString(text, "application/xml");
        let name = f.name;
        const metaName = xml.querySelector('metadata > name')?.textContent
                       || xml.querySelector('trk > name')?.textContent
                       || xml.querySelector('rte > name')?.textContent
                       || null;
        if (metaName) name = metaName;
        addTrack(geojson, xml, name);
      } catch (err) {
        showAlert('Błąd wczytywania pliku "' + f.name + '": ' + err.message, 'danger', 7000);
      }
    }
    // wyczyść input, żeby można było ponownie wczytać te same pliki jeśli potrzeba
    gpxFilesInput.value = '';
  });

  // ---------- Wczytanie z URL ----------
  loadUrlBtn.addEventListener('click', async () => {
    const url = gpxUrlInput.value.trim();
    if (!url) return showAlert('Podaj URL pliku GPX.', 'warning');
    try {
      showAlert('Pobieram: ' + url, 'info', 2500);
      const res = await fetch(url);
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const text = await res.text();
      const { geojson } = await parseGpxText(text);
      // spróbuj wyciągnąć nazwę z XML
      const parser = new DOMParser();
      const xml = parser.parseFromString(text, "application/xml");
      let name = url.split('/').pop() || url;
      const metaName = xml.querySelector('metadata > name')?.textContent
                       || xml.querySelector('trk > name')?.textContent
                       || xml.querySelector('rte > name')?.textContent
                       || null;
      if (metaName) name = metaName;
      addTrack(geojson, xml, name);
    } catch (err) {
      showAlert('Błąd pobierania/parowania z URL: ' + err.message, 'danger', 7000);
      console.error(err);
    }
    gpxUrlInput.value = '';
  });

  // ---------- Clear all ----------
  clearAllBtn.onclick = () => {
    for (const t of tracks) {
      try { map.removeLayer(t.layer); } catch(e){}
    }
    tracks.length = 0;
    refreshTracksList();
    updateChart();
    const tbody = document.querySelector("#summaryTable tbody");
    tbody.innerHTML = '';  
    showAlert('Usunięto wszystkie trasy', 'secondary', 2000);
  };

  // ---------- Przydatne: zoom to all ----------
  function fitAllTracks() {
    let allBounds = null;
    for (const t of tracks) {
      if (!t.bounds || !t.bounds.isValid()) continue;
      if (!allBounds) allBounds = t.bounds;
      else allBounds = allBounds.extend(t.bounds);
    }
    if (allBounds && allBounds.isValid()) map.fitBounds(allBounds.pad(0.1));
  }

  // ---------- Obsługa drag & drop (opcjonalnie) ----------
  ;(function enableDragDrop(){
    const dropArea = document.body;
    dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.classList.add('dragging'); });
    dropArea.addEventListener('dragleave', (e) => { dropArea.classList.remove('dragging'); });
    dropArea.addEventListener('drop', async (e) => {
      e.preventDefault();
      dropArea.classList.remove('dragging');
      const dt = e.dataTransfer;
      if (!dt) return;
      const files = Array.from(dt.files || []).filter(f => f.name && f.name.toLowerCase().endsWith('.gpx'));
      if (files.length) {
        // przypisz do input i wywołaj change
        gpxFilesInput.files = dt.files;
        const ev = new Event('change');
        gpxFilesInput.dispatchEvent(ev);
      } else {
        showAlert('Upuść plik(i) GPX lub użyj formularza.', 'info', 2500);
      }
    });
  })();

  // ---------- gotowe ----------
  showAlert('Gotowe. Wczytaj plik GPX lub podaj URL.', 'info', 4000);

L.control.fullscreen({
  position: 'topleft',
  title: {
      'false': 'Pełny ekran',  // tekst, gdy NIE jest w trybie fullscreen
      'true': 'Wyjdź z pełnego ekranu'  // tekst, gdy JUŻ jest fullscreen
  }
}).addTo(map);

</script>

Kod po stronie serwera

Informacja

Aplikacja nie korzysta z kodu po stronie serwera.

Tagi

JavaScript GPX OSM

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.