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.

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" />
<style>
    #app { overflow-x: hidden; }
    #map { height: 80vh; border-radius: 10px; }
    #sidebar {
      border-radius: 10px;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
      padding: 1rem;
      max-height: 80vh;
      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); }
    [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">
    <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>
  </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://cdnjs.cloudflare.com/ajax/libs/togeojson/0.16.0/togeojson.min.js"></script>

<script>
const map = L.map('map').setView([52.2297, 21.0122], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; OpenStreetMap'
}).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: 'blue'
            }
        }
    }
});
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 updatePointsList() {
    const container = document.getElementById('pointsList');
    const pts = getAllPoints();

    if (!pts.length) {
        container.innerHTML = "Brak punktów.";
        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);
    });
}

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(' ')}
    </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(' ')}
    </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: 'text-bg-info'
            }))
            .catch(err => BootstrapToast.show({
                title: 'Niepowodzenie',
                message: "Błąd kopiowania: " + err,
                when: 'teraz',
                type: 'text-bg-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: 'text-bg-info'
            });            
        } catch (err) {
            BootstrapToast.show({
                title: 'Niepowodzenie',
                message: "Nie udało się skopiować: " + err,
                when: 'teraz',
                type: 'text-bg-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

Informacja

Aplikacja nie korzysta z kodu po stronie serwera.

Tagi

GPX KML JavaScript

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.