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.
- 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: '© Esri, Maxar, Earthstar Geographics'
});
const esriTopo = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri'
});
const openTopo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxZoom: 17,
attribution: '© OpenTopoMap contributors'
});
// Nakładki tematyczne
const railways = L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', {
attribution: '© OpenRailwayMap contributors'
});
const seaports = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
attribution: '© OpenSeaMap contributors'
});
const hiking = L.tileLayer('https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', {
attribution: '© 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.