Przejdź do głównej treści

Kreator Tras GPX / KML

Grafika SVG Trasa

Twórz własne trasy GPS w kilka sekund!
Dzięki tej prostej aplikacji możesz w intuicyjny sposób rysować trasy na mapie OpenStreetMap, edytować je, a następnie eksportować do plików GPX lub KML — zgodnych z popularnymi aplikacjami i urządzeniami GPS (np. Garmin, Strava, Locus, Komoot).

Dlaczego warto skorzystać?

  • Działa w przeglądarce — bez instalacji, w pełni lokalnie.
  • Intuicyjna obsługa — przeciągnij, kliknij i gotowe!
  • Eksport do GPX/KML — trasy możesz wykorzystać w dowolnym oprogramowaniu GPS.
  • Import istniejących tras — wczytaj plik GPX/KML i edytuj go na mapie.
  • Edycja i czyszczenie trasy — usuwaj, dodawaj lub przesuwaj punkty.
  • Podgląd punktów — w bocznym panelu zobaczysz listę wszystkich współrzędnych.

Jak korzystać z aplikacji

  1. Na mapie pojawi się pasek narzędzi po lewej stronie:
    • Kliknij ikonę linii (Polyline), aby rozpocząć rysowanie trasy.
    • Klikaj na mapie, by dodawać kolejne punkty trasy.
    • Kliknij „Zakończ”, aby zapisać linię.
  2. W panelu bocznym po prawej stronie zobaczysz:
    • Listę punktów z ich współrzędnymi,
    • Możliwość kliknięcia, aby przejść do danego punktu,
  3. Użyj przycisków poniżej mapy:
    • Importuj GPX/KML — wczytaj istniejącą trasę,
    • Eksportuj GPX — zapisz trasę w formacie GPX,
    • Eksportuj KML — zapisz trasę w formacie KML,
    • Wyczyść trasę — usuń wszystkie punkty i zacznij od nowa.
  4. Po edycji możesz plik zachować na komputerze i wgrać go do swojej aplikacji GPS.

Wskazówki

  • Kliknij punkt w panelu bocznym, aby przybliżyć mapę do jego lokalizacji.
  • Możesz przeciągać punkty i linie, by precyzyjnie ustalić trasę.
  • Import działa zarówno dla plików GPX, jak i KML (np. z Garmina, Stravy lub Google Earth).
  • Wszystkie dane przetwarzane są lokalnie w Twojej przeglądarce — nie są nigdzie wysyłane.
  • Aplikacja łączy nowo dodane odcinki trasy eliminujac przerwy między nimi. Nie musisz dokładnie szukać punktu końcowego ostatniego odcinka.
Uwaga
  • Aplikacja nie uwzględnia wysokości terenu – trasy są rysowane w oparciu o współrzędne poziome (2D). Pliki GPX i KML nie zawierają danych o przewyższeniach (elevation).
  • Przyjmujemy, że teren jest płaski, dlatego dystans i szacowany profil trasy mogą się różnić od rzeczywistych.
  • Przyjmujemy, że pieszy porusza się ze średnią prędkością 5 km/h, rower 20 km/h a samochód 60 km/h.
  • Przejezdność dróg (np. stan nawierzchni, błoto, zamknięcia leśne, szlabany, prywatne tereny) nie jest weryfikowana automatycznie – użytkownik powinien sprawdzić trasę przed wyjazdem.
  • Dane mapowe pochodzą z OpenStreetMap i mogą zawierać błędy lub nieaktualne informacje.
  • Aplikacja nie uwzględnia warunków pogodowych ani pory roku (np. zalane odcinki, śnieg).
  • Używaj trasy na własną odpowiedzialność i zawsze przestrzegaj lokalnych przepisów ruchu oraz zasad poruszania się po lasach i terenach prywatnych.

Długość trasy

Czas przejazdu

---
---
---

Podgląd pliku

Kod po stronie przeglądarki

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<link rel="stylesheet" href="https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" />
<link type="text/css" href="http://www.dariuszrorat.ugu.pl/assets/css/bootstrap/wcag-outline.min.css" rel="stylesheet"> 
<style>
    #app { overflow-x: hidden; }
    #map { height: 80vh; border-radius: 10px; }
    #sidebar {
      padding: 1rem;
      max-height: 50vh;
      overflow-y: auto;
    }
    
    .point-item {
      padding: 0.3rem 0.6rem;
      margin-bottom: 0.4rem;
      border: 1px solid var(--bs-gray-500);
      border-radius: 8px;
      cursor: pointer;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .point-item:hover { background: var(--bs-body-bg-alt); }
    
    /* Naprawia widoczność tekstu w polu wyszukiwania */
    .leaflet-control-geocoder-form input {
      color: black !important;        /* kolor tekstu wpisywanego */
      background-color: white !important; /* kolor tła pola */
      font-size: 14px;
      padding: 4px 8px;
    }

    /* Opcjonalnie: popraw wygląd wyników */
    .leaflet-control-geocoder-alternatives li {
      background-color: white;
      color: black;
      padding: 4px 6px;
      border-bottom: 1px solid #ccc;
    }
    .leaflet-control-geocoder-alternatives li:hover {
      background-color: #eee;
    }    
</style>

<div id="app">
    
  <div class="row g-3">
    <div class="col-lg-9">
      <div id="map"></div>
      <div class="mt-3 d-flex justify-content-center gap-2 flex-wrap">        
        <button id="importBtn" class="btn btn-secondary"><i class="bi bi-upload"></i> Importuj GPX/KML</button>
        <button id="clearBtn" class="btn btn-danger"><i class="bi bi-x-circle"></i> Wyczyść trasę</button>
        <button id="exportGPX" class="btn btn-primary"><i class="bi bi-download"></i> Eksportuj GPX</button>
        <button id="exportKML" class="btn btn-success"><i class="bi bi-download"></i> Eksportuj KML</button>
      </div>
    </div>

    <div class="col-lg-3">
      <div id="sidebar">
        <h3 class="h5 mb-3"><i class="bi bi-pin-map"></i> Punkty trasy</h3>
        <div id="pointsList" class="small text-muted">
          Brak punktów.
        </div>
      </div>
      <div class="px-3 mt-3">
        <h3 class="h5"><i class="bi bi-rulers"></i> Długość trasy</h3>  
        <div id="totalDistance"></div>            
      </div>
      <div class="px-3 mt-3">
          <h3 class="h5"><i class="bi bi-stopwatch"></i> Czas przejazdu</h3>  
          <div>
            <i class="bi bi-car-front"></i> <span id="carTime">---</span>
          </div>
          <div>
            <i class="bi bi-bicycle"></i> <span id="bikeTime">---</span>
          </div>
          <div>
            <i class="bi bi-person-walking"></i> <span id="walkTime">---</span>
          </div>
      </div>  
    </div>
  </div>

  <div class="mt-4">
    <h3 class="h5">Podgląd pliku</h3>
    <div class="d-flex align-items-center gap-2 mb-2">
      <label for="previewFormat" class="form-label mb-0">Format:</label>
      <select id="previewFormat" class="form-select w-auto">
        <option value="gpx" selected>GPX</option>
        <option value="kml">KML</option>
      </select>
      <button id="copyPreview" class="btn btn-outline-secondary btn-sm">Kopiuj</button>
    </div>
    <pre><code id="filePreview" class="language-xml" style="max-height: 400px; overflow-y: auto;"></code></pre>
  </div>  
    
</div>         
<!-- Scripts -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.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://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>

<script>
  const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '© OpenStreetMap'
  });
  const esriSat = L.tileLayer(
      'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
          attribution: '&copy; Esri, Maxar, Earthstar Geographics'
      });
  const esriTopo = L.tileLayer(
      'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {
          attribution: '&copy; Esri'
      });
  const openTopo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
      maxZoom: 17,
      attribution: '&copy; OpenTopoMap contributors'
  });

  // Nakładki tematyczne
  const railways = L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', {
      attribution: '&copy; OpenRailwayMap contributors'
  });
  const seaports = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
      attribution: '&copy; OpenSeaMap contributors'
  });
  const hiking = L.tileLayer('https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', {
      attribution: '&copy; waymarkedtrails.org'
  });

  const map = L.map('map', {
      center: [52.1, 19.4],
      zoom: 6,
      layers: [osm]
  });

  osm.addTo(map);

  const baseLayers = {
      "OpenStreetMap": osm,
      "Satelita (Esri)": esriSat,
      "Topograficzna (Esri)": esriTopo,
      "Teren (OpenTopoMap)": openTopo
  };
  const overlays = {
      "Linie kolejowe": railways,
      "Dane morskie i porty": seaports,
      "Szlaki turystyczne": hiking
  };

  L.control.layers(baseLayers, overlays).addTo(map);

  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);

  L.control.scale({
      position: 'bottomleft', // domyślnie 'bottomleft', możesz dać 'bottomright' itd.
      metric: true, // pokazuj metry/km
      imperial: false, // ukryj mile
      maxWidth: 200 // maksymalna długość skali w pikselach
  }).addTo(map);

  // Dodaj wyszukiwarkę miast / adresów
  L.Control.geocoder({
    position: 'topleft',
    defaultMarkGeocode: true, // automatycznie dodaje marker i zoom
    placeholder: 'Szukaj miasta...',
    errorMessage: 'Nie znaleziono'
  }).addTo(map);
    
  let drawnItems = new L.FeatureGroup();
  map.addLayer(drawnItems);
  var lastEndPoint = null;

  const drawControl = new L.Control.Draw({
      edit: {
          featureGroup: drawnItems
      },
      draw: {
          polygon: false,
          rectangle: false,
          circle: false,
          circlemarker: false,
          marker: true,
          polyline: {
              shapeOptions: {
                  color: '#0d6efd'
              }
          }
      }
  });
  map.addControl(drawControl);

  map.on(L.Draw.Event.CREATED, e => {
      drawnItems.addLayer(e.layer);
      updatePointsList();
  });
  map.on(L.Draw.Event.EDITED, updatePointsList);
  map.on(L.Draw.Event.DELETED, updatePointsList);

  function getAllPoints() {
      const latlngs = [];
      drawnItems.eachLayer(layer => {
          if (layer instanceof L.Polyline) layer.getLatLngs().forEach(p => latlngs.push(p));
          else if (layer instanceof L.Marker) latlngs.push(layer.getLatLng());
      });
      return latlngs;
  }

  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));
  }

  /**
   * Szacuje czas przejazdu/przejścia dla danej odległości.
   * @param {number} distanceKm - dystans w kilometrach
   * @param {"car" | "bike" | "walk"} mode - tryb podróży
   * @returns {string} czas w formacie "X h Y min"
   */
  function estimateTravelTime(distanceKm, mode) {
    const speeds = {
      car: 60,   // km/h
      bike: 20,
      walk: 5
    };

    if (!speeds[mode]) {
      throw new Error("Nieznany tryb podróży. Użyj: car, bike, walk.");
    }

    const hours = distanceKm / speeds[mode];
    const h = Math.floor(hours);
    const m = Math.round((hours - h) * 60);

    return `${h} h ${m} min`;
  }
  
  function updatePointsList() {
      const container = document.getElementById('pointsList');
      const totalDist = document.getElementById('totalDistance');
      const carTime = document.getElementById('carTime');
      const bikeTime = document.getElementById('bikeTime');
      const walkTime = document.getElementById('walkTime');
      const pts = getAllPoints();
      let totalDistance = 0;
      let lastLat = 0;
      let lastLng = 0;

      if (!pts.length) {
          container.innerHTML = "Brak punktów.";
          totalDist.innerHTML = 'Brak trasy';
          carTime.innerHTML = '---';
          bikeTime.innerHTML = '---';
          walkTime.innerHTML = '---';
          return;
      }

      container.innerHTML = "";
      pts.forEach((p, idx) => {
          const div = document.createElement('div');
          div.className = "point-item";
          div.innerHTML = `
      <span><b>${idx + 1}.</b> ${p.lat.toFixed(5)}, ${p.lng.toFixed(5)}</span>      
    `;
          div.onclick = () => map.setView(p, 15);
          container.appendChild(div);
          if (idx > 0) {
              totalDistance += haversineDistanceKm(lastLat, lastLng, p.lat, p.lng);
          }
          lastLat = p.lat;
          lastLng = p.lng;
      });

      const carTravelTime = estimateTravelTime(totalDistance, 'car');
      const bikeTravelTime = estimateTravelTime(totalDistance, 'bike');
      const walkTravelTime = estimateTravelTime(totalDistance, 'walk');
      
      totalDist.innerHTML = totalDistance.toFixed(2) + ' km';
      carTime.innerHTML = carTravelTime;
      bikeTime.innerHTML = bikeTravelTime;
      walkTime.innerHTML = walkTravelTime;      
  }

  function removePoint(index) {
      drawnItems.eachLayer(layer => {
          if (layer instanceof L.Polyline) {
              let latlngs = layer.getLatLngs();
              if (latlngs.length > index) {
                  latlngs.splice(index, 1);
                  layer.setLatLngs(latlngs);
              }
          }
      });
      updatePointsList();
  }

  function download(filename, text) {
      const blob = new Blob([text], {
          type: 'text/xml'
      });
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = filename;
      link.click();
  }

  document.getElementById('exportGPX').onclick = () => {
      const pts = getAllPoints();
      if (!pts.length) return alert('Brak punktów trasy!');
      const gpx = `<?xml version="1.0"?>
<gpx version="1.1" creator="Kreator GPX">
  <trk>
    <name>Nowa trasa</name>
    <trkseg>
      ${pts.map(p => `<trkpt lat="${p.lat}" lon="${p.lng}"></trkpt>`).join('\n      ')}
    </trkseg>
  </trk>
</gpx>`;
      download('trasa.gpx', gpx);
  };

  document.getElementById('exportKML').onclick = () => {
      const pts = getAllPoints();
      if (!pts.length) return alert('Brak punktów trasy!');
      const kml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <name>Nowa trasa</name>
    <Placemark>
      <LineString>
        <coordinates>
          ${pts.map(p => `${p.lng},${p.lat},0`).join('\n          ')}
        </coordinates>
      </LineString>
    </Placemark>
  </Document>
</kml>`;
      download('trasa.kml', kml);
  };

  // aktualizacja listy na start
  updatePointsList();

  // === 🔁 Automatyczny podgląd GPX / KML z kolorowaniem ===

  const previewCode = document.getElementById('filePreview');
  const formatSelect = document.getElementById('previewFormat');
  const copyBtn = document.getElementById('copyPreview');

  function generatePreview() {
      const pts = getAllPoints();

      if (!pts.length) {
          previewCode.textContent = "// Brak punktów trasy do wyświetlenia";
          return;
      }

      let xml = "";

      if (formatSelect.value === "gpx") {
          xml = `<?xml version="1.0"?>
<gpx version="1.1" creator="Kreator GPX">
  <trk>
    <name>Podgląd trasy</name>
    <trkseg>
      ${pts.map(p => `<trkpt lat="${p.lat}" lon="${p.lng}"></trkpt>`).join('\n      ')}
    </trkseg>
  </trk>
</gpx>`;
      } else {
          xml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <name>Podgląd trasy</name>
    <Placemark>
      <LineString>
        <coordinates>
          ${pts.map(p => `${p.lng},${p.lat},0`).join('\n          ')}
        </coordinates>
      </LineString>
    </Placemark>
  </Document>
</kml>`;
      }

      if (previewCode.hasAttribute('data-highlighted'))
          previewCode.removeAttribute('data-highlighted');
      if (previewCode.hasAttribute('data-highlighter'))
          previewCode.removeAttribute('data-highlighter');
      if (previewCode.classList.contains('hljs'))
          previewCode.classList.remove(...Array.from(previewCode.classList).filter(cls => cls.startsWith('hljs')));

      previewCode.textContent = xml.trim();
      hljs.highlightElement(previewCode);
  }

  // 📋 Kopiowanie z fallbackiem
  copyBtn.onclick = () => {
      const text = previewCode.textContent;

      // Nowoczesny API (działa w HTTPS i localhost)
      if (navigator.clipboard && window.isSecureContext) {
          navigator.clipboard.writeText(text)
              .then(() => BootstrapToast.show({
                  title: 'Powiadomienie',
                  message: "Skopiowano kod " + formatSelect.value.toUpperCase() + " do schowka!",
                  when: 'teraz',
                  type: 'info'
              }))
              .catch(err => BootstrapToast.show({
                  title: 'Niepowodzenie',
                  message: "Błąd kopiowania: " + err,
                  when: 'teraz',
                  type: 'danger'
              }))
      } else {
          // Fallback dla plików lokalnych (file://)
          const temp = document.createElement("textarea");
          temp.value = text;
          document.body.appendChild(temp);
          temp.select();
          try {
              document.execCommand("copy");
              BootstrapToast.show({
                  title: 'Powiadomienie',
                  message: "Skopiowano kod " + formatSelect.value.toUpperCase() + " do schowka!",
                  when: 'teraz',
                  type: 'info'
              });
          } catch (err) {
              BootstrapToast.show({
                  title: 'Niepowodzenie',
                  message: "Nie udało się skopiować: " + err,
                  when: 'teraz',
                  type: 'danger'
              });
          }
          document.body.removeChild(temp);
      }
  };


  // 🔄 Aktualizacja przy zmianach
  formatSelect.onchange = generatePreview;
  document.addEventListener('DOMContentLoaded', generatePreview);

  // Pomocnicza funkcja do aktualizacji punktu końcowego
  function updateLastEndPoint() {
      const allPts = getAllPoints();
      lastEndPoint = allPts.length ? allPts[allPts.length - 1] : null;
  }
  // Obsługa rysowania nowego segmentu
  map.on(L.Draw.Event.CREATED, e => {
      const layer = e.layer;

      if (layer instanceof L.Polyline) {
          const latlngs = layer.getLatLngs();

          // jeśli istnieje wcześniejszy punkt końcowy — sklej
          if (lastEndPoint) {
              latlngs.unshift(lastEndPoint);
              layer.setLatLngs(latlngs);
          }

          drawnItems.addLayer(layer);
          updateLastEndPoint();
      } else if (layer instanceof L.Marker) {
          drawnItems.addLayer(layer);
          updateLastEndPoint();
      }

      updatePointsList();
      generatePreview();
  });

  // Po edycji — aktualizuj koniec trasy
  map.on(L.Draw.Event.EDITED, () => {
      updateLastEndPoint();
      updatePointsList();
      generatePreview();
  });

  // Po usunięciu — aktualizuj koniec trasy
  map.on(L.Draw.Event.DELETED, () => {
      updateLastEndPoint();
      updatePointsList();
      generatePreview();
  });

  // Po imporcie — ustaw koniec trasy
  function importTrack(xml, format) {
      let geojson;
      if (format === "gpx") geojson = toGeoJSON.gpx(xml);
      else if (format === "kml") geojson = toGeoJSON.kml(xml);
      else return alert("Nieobsługiwany format pliku.");

      drawnItems.clearLayers();

      geojson.features.forEach(f => {
          if (f.geometry.type === "LineString") {
              const coords = f.geometry.coordinates.map(c => [c[1], c[0]]);
              L.polyline(coords, {
                  color: "blue"
              }).addTo(drawnItems);
          } else if (f.geometry.type === "Point") {
              const [lng, lat] = f.geometry.coordinates;
              L.marker([lat, lng]).addTo(drawnItems);
          }
      });

      map.fitBounds(drawnItems.getBounds());
      updatePointsList();
      updateLastEndPoint(); // 🟢 kluczowe po imporcie
      generatePreview();
  }

  // Zmieniamy obsługę wczytywania pliku
  document.getElementById("importBtn").onclick = () => {
      const input = document.createElement("input");
      input.type = "file";
      input.accept = ".gpx,.kml";
      input.onchange = e => {
          const file = e.target.files[0];
          const reader = new FileReader();
          reader.onload = evt => {
              const text = evt.target.result;
              const xml = new DOMParser().parseFromString(text, "text/xml");
              importTrack(xml, file.name.endsWith(".gpx") ? "gpx" : "kml");
          };
          reader.readAsText(file);
      };
      input.click();
  };

  document.getElementById('clearBtn').onclick = () => {
      drawnItems.clearLayers();
      lastEndPoint = null; // resetuj punkt końcowy
      updatePointsList();
      generatePreview();
  };    
</script>

Kod po stronie serwera

Brak kodu serwera

Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.

15 października 2025 2

Kategorie

Technologie

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.