diff --git a/index.html b/index.html index ef6dadc..db3e6fa 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,7 @@

+

diff --git a/native/route_search.cpp b/native/route_search.cpp index 155c1b2..75780f5 100644 --- a/native/route_search.cpp +++ b/native/route_search.cpp @@ -489,7 +489,7 @@ float calculateRequiredSpeed(float endSpeed, float horizontalDistance, float hei return requiredSpeed; } -std::vector getPathJS(uint32_t startingNode, uint32_t endNode, float dragCoefficient) { +std::vector getPathJS(uint32_t startingNode, uint32_t endNode, float minimumSpeed, float dragCoefficient) { std::vector path; if (lastSearchResult.startingNode != startingNode) { @@ -529,8 +529,8 @@ std::vector getPathJS(uint32_t startingNode, uint32_t endNode, float float currentRequiredSpeed = -1.0; for (auto it = path.rbegin(); it != path.rend(); it++) { if (currentRequiredSpeed <= -1.0) { - it->requiredSpeed = 1.0; - currentRequiredSpeed = 1.0; + it->requiredSpeed = minimumSpeed; + currentRequiredSpeed = minimumSpeed; } else { uint32_t currentNodeId = it->nodeId; RoadNode currentNode = set.roadNodes[currentNodeId]; diff --git a/src/interfaces.ts b/src/interfaces.ts index 103341d..4c6f8eb 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -62,6 +62,7 @@ interface FoundPathsFromNode { interface GetFullPath { startNodeId: number, endNodeId: number, + minimumSpeed: number, dragCoefficient: number } diff --git a/src/main.ts b/src/main.ts index 43f5ebf..fd0733c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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') { diff --git a/src/modules/gpx/exporter.ts b/src/modules/gpx/exporter.ts new file mode 100644 index 0000000..e6b059b --- /dev/null +++ b/src/modules/gpx/exporter.ts @@ -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('' + new XMLSerializer().serializeToString(gpxDocument))); + element.setAttribute('download', 'freewheeling-route.gpx'); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); +} \ No newline at end of file diff --git a/src/modules/maphandler/maphandler.ts b/src/modules/maphandler/maphandler.ts index 51c7a00..d28461e 100644 --- a/src/modules/maphandler/maphandler.ts +++ b/src/modules/maphandler/maphandler.ts @@ -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); diff --git a/src/modules/worker/worker.ts b/src/modules/worker/worker.ts index 8712240..c2e9fdd 100644 --- a/src/modules/worker/worker.ts +++ b/src/modules/worker/worker.ts @@ -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;