diff --git a/index.html b/index.html index a6c0870..569c73c 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,9 @@
+
+ +

@@ -80,6 +83,10 @@ end points are shown as purple circles. Click an endpoint to see the calculated route that leads from the start point to the end point.

+

+ It is also possible to reverse the search, to find the farthest start point from a given end point. When this is done, + farthest possible start point is shown in green, and other candidate start points are shown in orange. +

It is possible to mark areas on the map you would like to avoid. Click the "Draw areas that should not be entered" button at the top left of the map. It is marked with this icon: . diff --git a/native/route_search.cpp b/native/route_search.cpp index 5266335..ab7c7ac 100644 --- a/native/route_search.cpp +++ b/native/route_search.cpp @@ -51,6 +51,7 @@ struct SearchResult { uint32_t startingNode; std::map previous; std::map reachableNodes; + bool reverse; }; struct JSNodeInfo { @@ -65,6 +66,7 @@ struct JSNodeInfo { struct JSSearchResult { std::vector endPoints; + bool reverse; }; struct ListNode { @@ -240,57 +242,51 @@ JSNodeInfo findClosestNode(float positionX, float positionY) { return result; } -float calculateSpeed(float startingSpeed, float horizontalDistance, float heightDifference, float minimumSpeed, float maximumSpeed, float dragCoefficient) { +float calculateSpeed(float startingSpeed, float horizontalDistance, float heightDifference, float dragCoefficient) { float slopeTan = heightDifference / horizontalDistance; - float finalSpeed = -1; // If the slope is flat, that is one calculation if (fabs(slopeTan) < 0.0001) { - float timeToFinish = (exp(horizontalDistance * dragCoefficient) - 1) / (startingSpeed * dragCoefficient); - finalSpeed = startingSpeed / (startingSpeed * dragCoefficient * timeToFinish + 1); - } else { - // Otherwise, we need to find some parameters - float slope = atan(slopeTan); - float slopeSin = sin(slope); - float fullDistance = horizontalDistance * slopeTan / slopeSin; - float acceleration = -GRAVITY_ACCELERATION * slopeSin; - float terminalVelocity = sqrt(fabs(acceleration) / dragCoefficient); - - // Uphill - if (slope > 0) { - float timeToPeak = atan(startingSpeed / terminalVelocity) / (dragCoefficient * terminalVelocity); - // If the discriminant is greater than 1, the slope is so steep that we cannot reach the end with our starting speed - float discriminant = cos(dragCoefficient * terminalVelocity * timeToPeak) * exp(fullDistance * dragCoefficient); - if (discriminant > 1.f) { - return -1; - } - - float timeToReachEnd = timeToPeak - acos(discriminant) / (dragCoefficient * terminalVelocity); - finalSpeed = terminalVelocity * tan(dragCoefficient * terminalVelocity * (timeToPeak - timeToReachEnd)); - } else { - // Downhill - // If the starting speed is very close to the terminal velocity, we'll just stay at terminal velocity - if (fabs(startingSpeed - terminalVelocity) < 0.001) { - finalSpeed = terminalVelocity; - } else if (startingSpeed < terminalVelocity) { - float k1 = terminalVelocity * log((terminalVelocity + startingSpeed) / (terminalVelocity - startingSpeed)) * 0.5; - float k2 = -log(cosh(k1 / terminalVelocity)) / dragCoefficient; - float timeSpent = acosh(exp(dragCoefficient * (fullDistance - k2))) / (dragCoefficient * terminalVelocity) - k1 / (dragCoefficient * pow(terminalVelocity, 2)); - finalSpeed = terminalVelocity * tanh(dragCoefficient * terminalVelocity * timeSpent + k1 / terminalVelocity); - } else if (startingSpeed > terminalVelocity) { - float k1 = log((startingSpeed - terminalVelocity) / (startingSpeed + terminalVelocity)) * terminalVelocity / 2; - float k2 = -log(-sinh(k1 / terminalVelocity)) / dragCoefficient; - float timeSpent = k1 / (dragCoefficient * pow(terminalVelocity, 2)) - asinh(-exp(dragCoefficient * (fullDistance - k2))) / (dragCoefficient * terminalVelocity); - finalSpeed = -terminalVelocity / tanh(k1 / terminalVelocity - dragCoefficient * terminalVelocity * timeSpent); - } + return startingSpeed * exp(-dragCoefficient * horizontalDistance); + } + + // If the slope is not flat, we should calculate some trig identities, and how long the slope is + float slope = atan(slopeTan); + float slopeSin = sin(slope); + float fullDistance = horizontalDistance * slopeTan / slopeSin; + + // We need to calculate the terminal velocity given the slope we're in + float terminalVelocity = sqrt(fabs(GRAVITY_ACCELERATION * slopeSin) / dragCoefficient); + + // First, calculate the final speed if we're going uphill + if (slope > 0) { + float timeToPeak = atan(startingSpeed / terminalVelocity) / (dragCoefficient * terminalVelocity); + float discriminant = exp(fullDistance * dragCoefficient) * cos(dragCoefficient * terminalVelocity * timeToPeak); + if (discriminant > 1.f) { + // If this value is greater than 1, it means that the slope is too steep and we can never reach the top with our + // starting speed + return -1; } + return terminalVelocity * tan(acos(discriminant)); } - - if (finalSpeed < minimumSpeed) { - return -1; - } else { - return std::fmin(finalSpeed, maximumSpeed); + + // Downhill must be split in three: starting slower than terminal velocity, starting at terminal velocity, and starting + // above terminal velocity + if (fabs(startingSpeed - terminalVelocity) < 0.0001) { + return terminalVelocity; } + + // If we're going faster than terminal velocity + if (startingSpeed > terminalVelocity) { + float k1 = log((startingSpeed - terminalVelocity) / (startingSpeed + terminalVelocity)); + float tanhInput = asinh(exp(dragCoefficient * fullDistance)*sinh(k1 * 0.5)); + return -terminalVelocity / tanh(tanhInput); + } + + // We only get here if we're going slower than terminal velocity + float k1 = log((terminalVelocity + startingSpeed) / (terminalVelocity - startingSpeed)); + float tanhInput = acosh(exp(dragCoefficient * fullDistance) * cosh(k1 * 0.5)); + return terminalVelocity * tanh(tanhInput); } float calculateRequiredSpeed(float endSpeed, float horizontalDistance, float heightDifference, float dragCoefficient) { @@ -352,8 +348,9 @@ void getNeighbourConnections(RoadNode node, Connection* targetArray, int &number } } -SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed) { +SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, bool reverse) { SearchResult result; + result.reverse = reverse; result.startingNode = startingNode; RoadNode firstNode = set.roadNodes[startingNode]; @@ -363,11 +360,11 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float m result.reachableNodes[startingNode] = firstNodeInfo; ListNode *nextNode = new ListNode; - + nextNode->id = startingNode; nextNode->currentSpeed = minimumSpeed; nextNode->currentCourse = 0; - + while (nextNode != NULL) { ListNode *currentNode = nextNode; nextNode = currentNode->next; @@ -408,12 +405,22 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float m RoadNode neighbourNode = set.roadNodes[neighbour.connectedPointNumber]; float heightDifference = neighbourNode.positionZ - bestNode.positionZ; - float resultingSpeed = calculateSpeed(currentSpeed, neighbour.distance, heightDifference, minimumSpeed, maximumSpeed, dragCoefficient); - - if (resultingSpeed < 0) { - continue; + float resultingSpeed = -1; + if (!reverse) { + resultingSpeed = calculateSpeed(currentSpeed, neighbour.distance, heightDifference, dragCoefficient); + if (resultingSpeed < minimumSpeed) { + continue; + } + resultingSpeed = fmin(resultingSpeed, maximumSpeed); + } else { + resultingSpeed = calculateRequiredSpeed(currentSpeed, neighbour.distance, -heightDifference, dragCoefficient); + if (resultingSpeed > maximumSpeed) { + continue; + } + resultingSpeed = fmax(resultingSpeed, minimumSpeed); } + // If we limit the speed on corners, do that here if (limitCornerSpeed) { float courseDifference = fabs(currentCourse - neighbour.course); @@ -421,18 +428,34 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float m courseDifference = 360 - courseDifference; } + float maximumCornerSpeed; if (courseDifference > 95) { - resultingSpeed = minimumSpeed; + maximumCornerSpeed = minimumSpeed; } else if (courseDifference > 45.0) { - float maximumCornerSpeed = (95 - courseDifference) / 50.0 * (maximumSpeed - minimumSpeed) + minimumSpeed; - resultingSpeed = fmin(resultingSpeed, maximumCornerSpeed); + maximumCornerSpeed = (95 - courseDifference) / 50.0 * (maximumSpeed - minimumSpeed) + minimumSpeed; + } else { + maximumCornerSpeed = maximumSpeed; + } + + if (reverse) { + if (resultingSpeed > maximumCornerSpeed) { + continue; + } + } else { + resultingSpeed = fmin(maximumCornerSpeed, resultingSpeed); } } // Check if this node is already in the reachable nodes map auto resultIterator = result.reachableNodes.find(neighbour.connectedPointNumber); - if (resultIterator != result.reachableNodes.end() && resultingSpeed <= resultIterator->second.currentSpeed) { - continue; + if (reverse) { + if (resultIterator != result.reachableNodes.end() && resultingSpeed >= resultIterator->second.currentSpeed) { + continue; + } + } else { + if (resultIterator != result.reachableNodes.end() && resultingSpeed <= resultIterator->second.currentSpeed) { + continue; + } } SearchNodeInfo reachableNodeInfo; @@ -447,20 +470,38 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float m neighbourListNode->currentSpeed = reachableNodeInfo.currentSpeed; neighbourListNode->currentCourse = neighbour.course; - if (nextNode == NULL || resultingSpeed < nextNode->currentSpeed) { - neighbourListNode->next = nextNode; - nextNode = neighbourListNode; - } else { - ListNode* previousSearchNode = nextNode; - ListNode* currentSearchNode = nextNode->next; - - while(currentSearchNode != NULL && currentSearchNode->currentSpeed > resultingSpeed) { - previousSearchNode = currentSearchNode; - currentSearchNode = currentSearchNode->next; + if (reverse) { + if (nextNode == NULL || resultingSpeed > nextNode->currentSpeed) { + neighbourListNode->next = nextNode; + nextNode = neighbourListNode; + } else { + ListNode* previousSearchNode = nextNode; + ListNode* currentSearchNode = nextNode->next; + + while (currentSearchNode != NULL && currentSearchNode->currentSpeed < resultingSpeed) { + previousSearchNode = currentSearchNode; + currentSearchNode = currentSearchNode->next; + } + + previousSearchNode->next = neighbourListNode; + neighbourListNode->next = currentSearchNode; + } + } else { + if (nextNode == NULL || resultingSpeed < nextNode->currentSpeed) { + neighbourListNode->next = nextNode; + nextNode = neighbourListNode; + } else { + ListNode* previousSearchNode = nextNode; + ListNode* currentSearchNode = nextNode->next; + + while(currentSearchNode != NULL && currentSearchNode->currentSpeed > resultingSpeed) { + previousSearchNode = currentSearchNode; + currentSearchNode = currentSearchNode->next; + } + + previousSearchNode->next = neighbourListNode; + neighbourListNode->next = currentSearchNode; } - - previousSearchNode->next = neighbourListNode; - neighbourListNode->next = currentSearchNode; } } } @@ -468,8 +509,8 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float m return result; } -JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed) { - lastSearchResult = findAllPathsFromPoint(startingNode, minimumSpeed, maximumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, limitCornerSpeed); +JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, bool reverse) { + lastSearchResult = findAllPathsFromPoint(startingNode, minimumSpeed, maximumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, limitCornerSpeed, reverse); float startX = set.roadNodes[startingNode].positionX; float startY = set.roadNodes[startingNode].positionY; @@ -520,6 +561,7 @@ JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, flo JSSearchResult searchResult; searchResult.endPoints = filteredEndpoints; + searchResult.reverse = lastSearchResult.reverse; return searchResult; } @@ -560,6 +602,10 @@ std::vector getPathJS(uint32_t startingNode, uint32_t endNode, float path.push_back(nodeInfo); } + if (lastSearchResult.reverse) { + std::reverse(path.begin(), path.end()); + } + float currentRequiredSpeed = -1.0; for (auto it = path.rbegin(); it != path.rend(); it++) { if (currentRequiredSpeed <= -1.0) { @@ -771,7 +817,8 @@ AreaSearchResult continueAreaSearch() { currentAreaSearch.allowMotorways, currentAreaSearch.allowTunnels, currentAreaSearch.allowAgainstOneway, - currentAreaSearch.limitCornerSpeed + currentAreaSearch.limitCornerSpeed, + false ); // Remove all nodes we have reached from here as possible future start nodes @@ -919,7 +966,8 @@ EMSCRIPTEN_BINDINGS(my_module) { emscripten::class_("SearchResult") .constructor<>() - .property("endPoints", &JSSearchResult::endPoints); + .property("endPoints", &JSSearchResult::endPoints) + .property("reverse", &JSSearchResult::reverse); emscripten::class_("PolygonCoordinate") .constructor<>() diff --git a/src/interfaces.ts b/src/interfaces.ts index f1e5ca7..4fb8736 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -45,7 +45,8 @@ interface FindPathsFromNode { allowMotorways: boolean, allowTunnels: boolean, allowAgainstOneway: boolean, - limitCornerSpeed: boolean + limitCornerSpeed: boolean, + reverse: boolean } export interface Endpoint { @@ -57,7 +58,8 @@ export interface Endpoint { interface FoundPathsFromNode { nodeId: number, - endpoints: Endpoint[] + endpoints: Endpoint[], + reverse: boolean } interface GetFullPath { diff --git a/src/main.ts b/src/main.ts index 6fd91ba..e5a2e51 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,7 +17,8 @@ interface Settings { allowTunnels: boolean, allowAgainstOneway: boolean, limitCornerSpeed: boolean, - cutoffDistance: number + cutoffDistance: number, + reverse: boolean } // Default settings values @@ -47,6 +48,7 @@ let loadingContainer = document.getElementById('loadingcontainer'); let mapContainer = document.getElementById('mapcontainer'); let searchButton = document.getElementById('search-button'); let exportButton = document.getElementById('export-route-button'); +let reverseCheckbox = document.getElementById('reverse-search-input'); let searchStatusParagraph = document.getElementById('search-status-paragraph'); let searchResultsTable = document.getElementById('search-result-table'); let searchResultTableBody = document.getElementById('search-result-table-body'); @@ -72,8 +74,8 @@ routeWorker.onmessage = e => { } else if (message.foundClosestNode != null) { - mapHandler?.drawStartNode(message.foundClosestNode.foundNodeId, message.foundClosestNode.foundLatitude, message.foundClosestNode.foundLongitude); let settings = getSettings(); + mapHandler?.drawStartNode(message.foundClosestNode.foundNodeId, message.foundClosestNode.foundLatitude, message.foundClosestNode.foundLongitude, settings.reverse); let findPathsMessage: Message = {findPathsFromNode: { nodeId: message.foundClosestNode.foundNodeId, minimumSpeed: settings.minimumSpeed, @@ -83,11 +85,12 @@ routeWorker.onmessage = e => { allowMotorways: settings.allowMotorways, allowTunnels: settings.allowTunnels, allowAgainstOneway: settings.allowAgainstOneway, - limitCornerSpeed: settings.limitCornerSpeed + limitCornerSpeed: settings.limitCornerSpeed, + reverse: settings.reverse }}; routeWorker.postMessage(findPathsMessage); } else if (message.foundPathsFromNode != null) { - mapHandler?.drawEndPoints(message.foundPathsFromNode.endpoints); + mapHandler?.drawEndPoints(message.foundPathsFromNode.endpoints, message.foundPathsFromNode.reverse); } else if (message.returnFullPath != null) { let settings = getSettings(); mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed); @@ -130,8 +133,7 @@ routeWorker.onmessage = e => { button.setHTMLUnsafe('Show in map'); button.addEventListener('click', _ => { - mapHandler?.drawStartNode(result.nodeId, result.latitude, result.longitude); - let settings = getSettings(); + mapHandler?.drawStartNode(result.nodeId, result.latitude, result.longitude, false); let requestMessage: Message = { findPathsFromNode: { nodeId: result.nodeId, @@ -142,7 +144,8 @@ routeWorker.onmessage = e => { allowMotorways: settings.allowMotorways, allowTunnels: settings.allowTunnels, allowAgainstOneway: settings.allowAgainstOneway, - limitCornerSpeed: settings.limitCornerSpeed + limitCornerSpeed: settings.limitCornerSpeed, + reverse: false } }; routeWorker.postMessage(requestMessage); @@ -195,7 +198,8 @@ function setUpMapHandler() { allowMotorways: settings.allowMotorways, allowTunnels: settings.allowTunnels, allowAgainstOneway: settings.allowAgainstOneway, - limitCornerSpeed: settings.limitCornerSpeed + limitCornerSpeed: settings.limitCornerSpeed, + reverse: settings.reverse }}; routeWorker.postMessage(newRoutesMessage); } @@ -325,7 +329,8 @@ function getSettings(): Settings { allowTunnels: getBooleanValue(allowTunnelsInput, DEFAULT_ALLOW_TUNNELS), allowAgainstOneway: getBooleanValue(allowAgainstOnewayInput, DEFAULT_ALLOW_AGAINST_ONE_WAY), limitCornerSpeed: getBooleanValue(limitCornerSpeedInput, DEFAULT_LIMIT_CORNER_SPEED), - cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE) + cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE), + reverse: getBooleanValue(reverseCheckbox, false) }; } diff --git a/src/modules/maphandler/maphandler.ts b/src/modules/maphandler/maphandler.ts index 1d58460..638bcf5 100644 --- a/src/modules/maphandler/maphandler.ts +++ b/src/modules/maphandler/maphandler.ts @@ -250,28 +250,44 @@ class MapHandler { this.searchAreaPolygonListeners.push(searchAreaListener); } - public drawStartNode(nodeId: number, latitude: number, longitude: number): void { + public drawStartNode(nodeId: number, latitude: number, longitude: number, reverse: boolean): void { this.path.clearLayers(); if (this.startMarker != null) { this.map.removeLayer(this.startMarker); } + var settings; + if (reverse) { + settings = {icon: redIcon}; + } else { + settings = {icon: greenIcon}; + } + this.currentStartPoint = nodeId; this.endMarkers.clearLayers(); - this.startMarker = L.marker([latitude, longitude], {icon: greenIcon}).addTo(this.map); + this.startMarker = L.marker([latitude, longitude], settings).addTo(this.map); } - public drawEndPoints(endpoints: Endpoint[]): void { + public drawEndPoints(endpoints: Endpoint[], reverse: boolean): void { this.endMarkers.clearLayers(); this.path.clearLayers(); var firstMarker = true; + + var color = reverse ? 'orange' : 'violet'; + var settings: L.MarkerOptions; + if (reverse) { + settings = {icon: greenIcon}; + } else { + settings = {icon: redIcon}; + } + endpoints.forEach(endpoint => { var marker; if (firstMarker) { - marker = L.marker([endpoint.latitude, endpoint.longitude], {icon: redIcon}).addTo(this.endMarkers); + marker = L.marker([endpoint.latitude, endpoint.longitude], settings).addTo(this.endMarkers); } else { - marker = L.circleMarker([endpoint.latitude, endpoint.longitude], {radius: 2, fillOpacity: 1.0, color: 'purple', bubblingMouseEvents: false}).addTo(this.endMarkers); + marker = L.circleMarker([endpoint.latitude, endpoint.longitude], {radius: 2, fillOpacity: 1.0, color: color, bubblingMouseEvents: false}).addTo(this.endMarkers); } marker.bindTooltip(Math.round(endpoint.distanceFromStart) + 'm'); diff --git a/src/modules/worker/worker.ts b/src/modules/worker/worker.ts index 64416de..96a9628 100644 --- a/src/modules/worker/worker.ts +++ b/src/modules/worker/worker.ts @@ -103,7 +103,8 @@ onmessage = async (e) => { message.findPathsFromNode.allowMotorways, message.findPathsFromNode.allowTunnels, message.findPathsFromNode.allowAgainstOneway, - message.findPathsFromNode.limitCornerSpeed + message.findPathsFromNode.limitCornerSpeed, + message.findPathsFromNode.reverse ); let endpoints: Endpoint[] = []; @@ -122,9 +123,10 @@ onmessage = async (e) => { distanceFromStart: nodeData.distanceFromStart }); } + let reverse: boolean = results.reverse; results.delete(); - let returnMessage: Message = {foundPathsFromNode: {nodeId: message.findPathsFromNode.nodeId, endpoints: endpoints}}; + let returnMessage: Message = {foundPathsFromNode: {nodeId: message.findPathsFromNode.nodeId, endpoints: endpoints, reverse: reverse}}; postMessage(returnMessage); }