Export routes to gpx

This commit is contained in:
Martin Asprusten
2025-07-02 01:10:22 +02:00
parent 00db764fd0
commit 06fa1be52b
7 changed files with 75 additions and 8 deletions
+1
View File
@@ -62,6 +62,7 @@ interface FoundPathsFromNode {
interface GetFullPath {
startNodeId: number,
endNodeId: number,
minimumSpeed: number,
dragCoefficient: number
}
+25 -2
View File
@@ -1,7 +1,8 @@
import './style.css';
import './modules/maphandler/maphandler'
import MapHandler from './modules/maphandler/maphandler';
import type { Message, Polygon } from './interfaces';
import type { Message, PathSegment, Polygon } from './interfaces';
import { downloadGpxFile } from './modules/gpx/exporter';
interface WindowState {
state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching'
@@ -34,6 +35,7 @@ var mapHandler: MapHandler | null = null;
var currentSearchArea: Polygon[] = [];
var currentState: WindowState = {state: 'DataNotLoaded'};
var lastRoute: PathSegment[] | undefined;
var lastSearchUpdate = Date.now();
@@ -42,6 +44,7 @@ let notLoadedContainer = document.getElementById('notloadedcontainer');
let loadingContainer = document.getElementById('loadingcontainer');
let mapContainer = document.getElementById('mapcontainer');
let searchButton = document.getElementById('search-button');
let exportButton = document.getElementById('export-route-button');
let searchStatusParagraph = document.getElementById('search-status-paragraph');
let searchResultsTable = document.getElementById('search-result-table');
let searchResultTableBody = document.getElementById('search-result-table-body');
@@ -84,6 +87,7 @@ routeWorker.onmessage = e => {
} else if (message.returnFullPath != null) {
let settings = getSettings();
mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed);
setRoute(message.returnFullPath.pathSegments);
} else if (message.searchAreaResult != null) {
searchStatusParagraph?.setHTMLUnsafe('Searching. ' + message.searchAreaResult.remainingNodes + ' possible starting points remain.');
let currentTime = Date.now();
@@ -158,10 +162,11 @@ function setUpMapHandler() {
mapHandler.addClickedMapListener((latitude, longitude) => {
let message: Message = {findClosestNode: {latitude: latitude, longitude: longitude}};
routeWorker.postMessage(message);
setRoute(undefined);
});
mapHandler.addClickedEndpointListener((startNodeId, endNodeId) => {
let settings = getSettings();
let message: Message = {getFullPath: {startNodeId: startNodeId, endNodeId: endNodeId, dragCoefficient: settings.dragCoefficient}};
let message: Message = {getFullPath: {startNodeId: startNodeId, endNodeId: endNodeId, minimumSpeed: settings.minimumSpeed, dragCoefficient: settings.dragCoefficient}};
routeWorker.postMessage(message);
});
mapHandler.addExclusionAreaListener(polygons => {
@@ -232,6 +237,18 @@ function setState(state: WindowState) {
};
setState({state: 'DataNotLoaded'});
function setRoute(route?: PathSegment[]) {
lastRoute = route;
if (exportButton && exportButton instanceof HTMLButtonElement) {
if (lastRoute == null) {
exportButton.disabled = true;
} else {
exportButton.disabled = false;
}
}
};
setRoute(undefined);
// Set up the settings values
function setUpNumberInput(element: HTMLElement | null, localStorageKey: string, defaultValue: number): void {
if (element != null && element instanceof HTMLInputElement) {
@@ -345,6 +362,12 @@ searchButton?.addEventListener('click', _ => {
}
});
exportButton?.addEventListener('click', _ => {
if (lastRoute != null) {
downloadGpxFile(lastRoute);
}
})
settingsButton?.addEventListener('click', _ => {
let settingsDisplay = settingsDiv?.style.getPropertyValue('display');
if (settingsDisplay === 'none') {
+42
View File
@@ -0,0 +1,42 @@
import type { Coordinate, PathSegment } from "../../interfaces";
function createAndAppendChild(document: XMLDocument, parent: HTMLElement, nameOfChild: string, contents?: string): HTMLElement {
let childElement = document.createElement(nameOfChild);
parent.appendChild(childElement);
if (contents) {
childElement.appendChild(document.createTextNode(contents));
}
return childElement;
}
function createRoutePoint(document: XMLDocument, parent: HTMLElement, coordinate: Coordinate, requiredSpeed: number) {
let routePoint = createAndAppendChild(document, parent, 'rtept');
routePoint.setAttribute('lat', coordinate.latitude.toFixed(6));
routePoint.setAttribute('lon', coordinate.longitude.toFixed(6));
createAndAppendChild(document, routePoint, 'desc', 'Required speed: ' + (requiredSpeed * 3.6).toFixed(1) + ' km/h');
}
export function downloadGpxFile(path: PathSegment[]): void {
let gpxDocument = document.implementation.createDocument('http://www.topografix.com/GPX/1/1', 'gpx');
let routeElement = createAndAppendChild(gpxDocument, gpxDocument.documentElement, 'rte');
createAndAppendChild(gpxDocument, routeElement, 'name', 'Freewheeling route');
createAndAppendChild(gpxDocument, routeElement, 'desc', 'Route to be biked without pedalling');
createAndAppendChild(gpxDocument, routeElement, 'src', 'https://freewheeling.martinserver.no');
// Add first route point
if (path.length > 0) {
createRoutePoint(gpxDocument, routeElement, path.at(0)!.start, path.at(0)!.requiredSpeed);
}
path.forEach(segment => {
createRoutePoint(gpxDocument, routeElement, segment.end, segment.requiredSpeed);
});
var element = document.createElement('a');
element.setAttribute('href', 'data:application/gpx+xml;base64,' + btoa('<?xml version="1.0" encoding="utf-8"?>' + new XMLSerializer().serializeToString(gpxDocument)));
element.setAttribute('download', 'freewheeling-route.gpx');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
+2 -2
View File
@@ -11,7 +11,7 @@ import type { Position } from 'geojson';
import { createPalette } from 'hue-map';
const viridisPalette = createPalette({
map: 'viridis',
map: 'autumn',
steps: 100
});
@@ -314,7 +314,7 @@ class MapHandler {
let intensity = Math.round((pathSegment.requiredSpeed - minimumSpeed) * 99.0 / (maximumSpeed - minimumSpeed));
intensity = Math.min(Math.max(intensity, 0), 99);
let color = viridisPalette.format('cssHex')[99-intensity];
let color = viridisPalette.format('cssHex')[intensity];
let polyLine = L.polyline(leafletCoordinates, {color: color}).addTo(this.path);
let requiredSpeed = Math.max(minimumSpeed, pathSegment.requiredSpeed);
+1 -1
View File
@@ -128,7 +128,7 @@ onmessage = async (e) => {
}
if (message.getFullPath != null) {
let path = module.getPath(message.getFullPath.startNodeId, message.getFullPath.endNodeId, message.getFullPath.dragCoefficient);
let path = module.getPath(message.getFullPath.startNodeId, message.getFullPath.endNodeId, message.getFullPath.minimumSpeed, message.getFullPath.dragCoefficient);
if (!path) {
sendErrorMessage('Could not get path');
return;