Allow reverse search, where you find the farthest start point from a given end point

This commit is contained in:
Martin Asprusten 2025-07-05 17:06:52 +02:00
parent 2ca8a14d8f
commit dc2e8170a4
6 changed files with 171 additions and 91 deletions

View File

@ -32,6 +32,9 @@
<br />
<button id="search-button">Start search</button>
<button id="export-route-button">Export KML route</button>
<br />
<label for="reverse-search-input">Reverse search:</label><input type="checkbox" id="reverse-search-input" />
<br />
<p id="search-status-paragraph"></p>
<table id="search-result-table" style="display: none;">
<thead>
@ -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.
</p>
<p>
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.
</p>
<p>
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: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="20px"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --> <path d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>.

View File

@ -51,6 +51,7 @@ struct SearchResult {
uint32_t startingNode;
std::map<uint32_t, uint32_t> previous;
std::map<uint32_t, SearchNodeInfo> reachableNodes;
bool reverse;
};
struct JSNodeInfo {
@ -65,6 +66,7 @@ struct JSNodeInfo {
struct JSSearchResult {
std::vector<JSNodeInfo> 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<JSNodeInfo> 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_<JSSearchResult>("SearchResult")
.constructor<>()
.property("endPoints", &JSSearchResult::endPoints);
.property("endPoints", &JSSearchResult::endPoints)
.property("reverse", &JSSearchResult::reverse);
emscripten::class_<PolygonCoordinate>("PolygonCoordinate")
.constructor<>()

View File

@ -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 {

View File

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

View File

@ -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');

View File

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