Kreator tras GPX / KML
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
- 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ę.
- W panelu bocznym po prawej stronie zobaczysz:
- Listę punktów z ich współrzędnymi,
- Możliwość kliknięcia, aby przejść do danego punktu,
- 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.
- 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: '© 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.