|
|
@@ -200,6 +200,7 @@ const Browser = (() => {
|
|
|
<button class="item-btn view-track-btn" data-id="${track.id}" title="View on map">👁️</button>
|
|
|
<button class="item-btn edit-track-btn" data-id="${track.id}" data-name="${escAttr(track.name)}" data-type="${escAttr(track.trackType || '')}" title="Edit">✏️</button>
|
|
|
<button class="item-btn share-track-btn" data-id="${track.id}" title="Share">🔗</button>
|
|
|
+ <button class="item-btn export-track-btn" data-id="${track.id}" data-name="${escAttr(track.name)}" title="Export GPX">⬇️</button>
|
|
|
<button class="item-btn move-track-btn" data-id="${track.id}" data-name="${escAttr(track.name)}" title="Move">📂</button>
|
|
|
<button class="item-btn delete-track-btn" data-id="${track.id}" data-name="${escAttr(track.name)}" title="Delete">🗑️</button>
|
|
|
</span>
|
|
|
@@ -296,6 +297,13 @@ const Browser = (() => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+ container.querySelectorAll('.export-track-btn').forEach(btn => {
|
|
|
+ btn.addEventListener('click', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ exportTrack(parseInt(btn.dataset.id), btn.dataset.name);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
container.querySelectorAll('.move-track-btn').forEach(btn => {
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
e.stopPropagation();
|
|
|
@@ -616,6 +624,48 @@ const Browser = (() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ async function exportTrack(trackId, trackName) {
|
|
|
+ try {
|
|
|
+ let data = trackDataCache[trackId];
|
|
|
+ if (!data) {
|
|
|
+ data = await API.getTrackPoints(trackId);
|
|
|
+ trackDataCache[trackId] = data;
|
|
|
+ }
|
|
|
+ const gpx = buildGpx(data);
|
|
|
+ const blob = new Blob([gpx], { type: 'application/gpx+xml' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = (trackName || 'track').replace(/[/\\:*?"<>|]/g, '_') + '.gpx';
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ } catch (e) {
|
|
|
+ showToast('Export failed: ' + e.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function buildGpx(data) {
|
|
|
+ const name = data.meta?.name || 'Track';
|
|
|
+ const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
|
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
|
+ xml += '<gpx version="1.1" creator="gpx-vis" xmlns="http://www.topografix.com/GPX/1/1">\n';
|
|
|
+ xml += ` <trk>\n <name>${esc(name)}</name>\n`;
|
|
|
+ for (const seg of (data.segments || [])) {
|
|
|
+ xml += ' <trkseg>\n';
|
|
|
+ for (const [lat, lon, ele, time] of seg) {
|
|
|
+ xml += ` <trkpt lat="${lat}" lon="${lon}">`;
|
|
|
+ if (ele != null) xml += `<ele>${ele}</ele>`;
|
|
|
+ if (time) xml += `<time>${time}</time>`;
|
|
|
+ xml += '</trkpt>\n';
|
|
|
+ }
|
|
|
+ xml += ' </trkseg>\n';
|
|
|
+ }
|
|
|
+ xml += ' </trk>\n</gpx>\n';
|
|
|
+ return xml;
|
|
|
+ }
|
|
|
+
|
|
|
// ===== Dir Actions =====
|
|
|
|
|
|
async function createDirPrompt() {
|