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

View File

@ -31,6 +31,7 @@
<div id="map"></div> <div id="map"></div>
<br /> <br />
<button id="search-button">Start search</button> <button id="search-button">Start search</button>
<button id="export-route-button">Export GPX route</button>
<p id="search-status-paragraph"></p> <p id="search-status-paragraph"></p>
<table id="search-result-table" style="display: none;"> <table id="search-result-table" style="display: none;">
<thead> <thead>

View File

@ -489,7 +489,7 @@ float calculateRequiredSpeed(float endSpeed, float horizontalDistance, float hei
return requiredSpeed; return requiredSpeed;
} }
std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float dragCoefficient) { std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float minimumSpeed, float dragCoefficient) {
std::vector<JSNodeInfo> path; std::vector<JSNodeInfo> path;
if (lastSearchResult.startingNode != startingNode) { if (lastSearchResult.startingNode != startingNode) {
@ -529,8 +529,8 @@ std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float
float currentRequiredSpeed = -1.0; float currentRequiredSpeed = -1.0;
for (auto it = path.rbegin(); it != path.rend(); it++) { for (auto it = path.rbegin(); it != path.rend(); it++) {
if (currentRequiredSpeed <= -1.0) { if (currentRequiredSpeed <= -1.0) {
it->requiredSpeed = 1.0; it->requiredSpeed = minimumSpeed;
currentRequiredSpeed = 1.0; currentRequiredSpeed = minimumSpeed;
} else { } else {
uint32_t currentNodeId = it->nodeId; uint32_t currentNodeId = it->nodeId;
RoadNode currentNode = set.roadNodes[currentNodeId]; RoadNode currentNode = set.roadNodes[currentNodeId];

View File

@ -62,6 +62,7 @@ interface FoundPathsFromNode {
interface GetFullPath { interface GetFullPath {
startNodeId: number, startNodeId: number,
endNodeId: number, endNodeId: number,
minimumSpeed: number,
dragCoefficient: number dragCoefficient: number
} }

View File

@ -1,7 +1,8 @@
import './style.css'; import './style.css';
import './modules/maphandler/maphandler' import './modules/maphandler/maphandler'
import MapHandler from './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 { interface WindowState {
state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching' state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching'
@ -34,6 +35,7 @@ var mapHandler: MapHandler | null = null;
var currentSearchArea: Polygon[] = []; var currentSearchArea: Polygon[] = [];
var currentState: WindowState = {state: 'DataNotLoaded'}; var currentState: WindowState = {state: 'DataNotLoaded'};
var lastRoute: PathSegment[] | undefined;
var lastSearchUpdate = Date.now(); var lastSearchUpdate = Date.now();
@ -42,6 +44,7 @@ let notLoadedContainer = document.getElementById('notloadedcontainer');
let loadingContainer = document.getElementById('loadingcontainer'); let loadingContainer = document.getElementById('loadingcontainer');
let mapContainer = document.getElementById('mapcontainer'); let mapContainer = document.getElementById('mapcontainer');
let searchButton = document.getElementById('search-button'); let searchButton = document.getElementById('search-button');
let exportButton = document.getElementById('export-route-button');
let searchStatusParagraph = document.getElementById('search-status-paragraph'); let searchStatusParagraph = document.getElementById('search-status-paragraph');
let searchResultsTable = document.getElementById('search-result-table'); let searchResultsTable = document.getElementById('search-result-table');
let searchResultTableBody = document.getElementById('search-result-table-body'); let searchResultTableBody = document.getElementById('search-result-table-body');
@ -84,6 +87,7 @@ routeWorker.onmessage = e => {
} else if (message.returnFullPath != null) { } else if (message.returnFullPath != null) {
let settings = getSettings(); let settings = getSettings();
mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed); mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed);
setRoute(message.returnFullPath.pathSegments);
} else if (message.searchAreaResult != null) { } else if (message.searchAreaResult != null) {
searchStatusParagraph?.setHTMLUnsafe('Searching. ' + message.searchAreaResult.remainingNodes + ' possible starting points remain.'); searchStatusParagraph?.setHTMLUnsafe('Searching. ' + message.searchAreaResult.remainingNodes + ' possible starting points remain.');
let currentTime = Date.now(); let currentTime = Date.now();
@ -158,10 +162,11 @@ function setUpMapHandler() {
mapHandler.addClickedMapListener((latitude, longitude) => { mapHandler.addClickedMapListener((latitude, longitude) => {
let message: Message = {findClosestNode: {latitude: latitude, longitude: longitude}}; let message: Message = {findClosestNode: {latitude: latitude, longitude: longitude}};
routeWorker.postMessage(message); routeWorker.postMessage(message);
setRoute(undefined);
}); });
mapHandler.addClickedEndpointListener((startNodeId, endNodeId) => { mapHandler.addClickedEndpointListener((startNodeId, endNodeId) => {
let settings = getSettings(); 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); routeWorker.postMessage(message);
}); });
mapHandler.addExclusionAreaListener(polygons => { mapHandler.addExclusionAreaListener(polygons => {
@ -232,6 +237,18 @@ function setState(state: WindowState) {
}; };
setState({state: 'DataNotLoaded'}); 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 // Set up the settings values
function setUpNumberInput(element: HTMLElement | null, localStorageKey: string, defaultValue: number): void { function setUpNumberInput(element: HTMLElement | null, localStorageKey: string, defaultValue: number): void {
if (element != null && element instanceof HTMLInputElement) { if (element != null && element instanceof HTMLInputElement) {
@ -345,6 +362,12 @@ searchButton?.addEventListener('click', _ => {
} }
}); });
exportButton?.addEventListener('click', _ => {
if (lastRoute != null) {
downloadGpxFile(lastRoute);
}
})
settingsButton?.addEventListener('click', _ => { settingsButton?.addEventListener('click', _ => {
let settingsDisplay = settingsDiv?.style.getPropertyValue('display'); let settingsDisplay = settingsDiv?.style.getPropertyValue('display');
if (settingsDisplay === 'none') { if (settingsDisplay === 'none') {

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);
}

View File

@ -11,7 +11,7 @@ import type { Position } from 'geojson';
import { createPalette } from 'hue-map'; import { createPalette } from 'hue-map';
const viridisPalette = createPalette({ const viridisPalette = createPalette({
map: 'viridis', map: 'autumn',
steps: 100 steps: 100
}); });
@ -314,7 +314,7 @@ class MapHandler {
let intensity = Math.round((pathSegment.requiredSpeed - minimumSpeed) * 99.0 / (maximumSpeed - minimumSpeed)); let intensity = Math.round((pathSegment.requiredSpeed - minimumSpeed) * 99.0 / (maximumSpeed - minimumSpeed));
intensity = Math.min(Math.max(intensity, 0), 99); 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 polyLine = L.polyline(leafletCoordinates, {color: color}).addTo(this.path);
let requiredSpeed = Math.max(minimumSpeed, pathSegment.requiredSpeed); let requiredSpeed = Math.max(minimumSpeed, pathSegment.requiredSpeed);

View File

@ -128,7 +128,7 @@ onmessage = async (e) => {
} }
if (message.getFullPath != null) { 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) { if (!path) {
sendErrorMessage('Could not get path'); sendErrorMessage('Could not get path');
return; return;