Initial commit of a working website
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
dist/
|
||||
native/*
|
||||
node_modules/
|
||||
!native/route_search.cpp
|
||||
12
README.md
@ -1,3 +1,13 @@
|
||||
# Freewheeling
|
||||
|
||||
A website for calculating how far you can ride your bike without pedalling
|
||||
A website for calculating how far you can ride your bike without pedalling.
|
||||
|
||||
To run, it requires a file named 'roads.dat', which contains road data for all of Norway. This file is almost 700 MB large, so it is not included here. It can be downloaded from the [Freewheeling website.](https://freewheeling.martinserver.no) At some point, I may add the code for generating the file to this repository.
|
||||
|
||||
This repository requires emscripten in order to build webassembly from the C++ code in the native folder. Before you can run npm run build or npm run build:emscripten, the emcc binary needs to be added to the PATH.
|
||||
|
||||
## To run
|
||||
First, run npm run build:emscripten, then run npx vite.
|
||||
|
||||
## To build
|
||||
Run npm run build. This will first run the emscripten build, and then the typescript build. The resulting website ends up in the dist/ folder
|
||||
92
index.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="././static/bicycle-solid.svg" />
|
||||
<title>Freewheeling</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<h1 id="freewheeling-header">Freewheeling</h1>
|
||||
<div id="notloadedcontainer">
|
||||
<p>
|
||||
This web page allows you to calculate how far you can roll on a bicycle without using the pedals. It uses road data for
|
||||
all of Norway, downloaded from <a href="https://kartkatalog.geonorge.no/metadata/elveg-20/77944f7e-3d75-4f6d-ae04-c528cc72e8f6">Kartkatalogen at Geonorge</a>.
|
||||
Since I don't have a very powerful server, all calculations are done locally in your browser.
|
||||
</p>
|
||||
<p>
|
||||
These calculations require a data file containing road data for Norway. Since this is a large data file (of about
|
||||
700 MB), this file is not downloaded automatically when the page is loaded. Instead, it can be manually downloaded from
|
||||
<a href="./roads.dat">here.</a> Once the file is downloaded, use the file browser below to provide it to the web page.
|
||||
Note that this does not upload the file anywhere, it just tells the browser to load this file.
|
||||
</p>
|
||||
<label for="data-file-chooser">File containing road data: </label><input id="data-file-chooser" type="file" />
|
||||
</div>
|
||||
<div id="loadingcontainer">
|
||||
<p>Loading data...</p>
|
||||
<p>This might take a few seconds.</p>
|
||||
</div>
|
||||
<div id="mapcontainer">
|
||||
<div id="map"></div>
|
||||
<br />
|
||||
<button id="search-button">Start search</button>
|
||||
<p id="search-status-paragraph"></p>
|
||||
<table id="search-result-table" style="display: none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Distance</th><th>Latitude</th><th>Longitude</th><th>Show in map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="search-result-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<button id="settings-button">Settings</button>
|
||||
<div id="settings-div" style="display: none;">
|
||||
<div class="settings-line">
|
||||
<label for="minimum-speed-input">Minimum speed (km/h):</label><input id="minimum-speed-input" type="number" min="0" step="1" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="maximum-speed-input">Maximum speed (km/h):</label><input id="maximum-speed-input" type="number" min="0" step="1" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="maximum-speed-limit-input">Maximum speed of road (km/h):</label><input id="maximum-speed-limit-input" type="number" min="0" step="10" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="allow-motorways-input">Allow motorways:</label><input id="allow-motorways-input" type="checkbox" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="allow-tunnels-input">Allow tunnels:</label><input id="allow-tunnels-input" type="checkbox" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="allow-wrong-way-input">Allow going the wrong way down one-way roads:</label><input id="allow-wrong-way-input" type="checkbox" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="drag-coefficient-input">Drag coeffient (0.005 is a good default):</label><input id="drag-coefficient-input" type="number" min="0" step="0.001" />
|
||||
</div>
|
||||
<div class="settings-line">
|
||||
<label for="cutoff-distance-input">Cut-off distance when searching (m):</label><input id="cutoff-distance-input" type="number" min="0" step="1000" />
|
||||
</div>
|
||||
</div>
|
||||
<h2>Instructions:</h2>
|
||||
<p>
|
||||
Click anywhere on the map to calculate where it is possible to freewheel from that point. A set of possible
|
||||
end points will then appear on the map. The farthest possible end point is shown in red, while other possible
|
||||
end points are shown in violet. Click an endpoint to see the calculated route that leads from the start point
|
||||
to the end point.
|
||||
</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>.
|
||||
Any routes calculated will then avoid the areas drawn.
|
||||
</p>
|
||||
<p>
|
||||
It is also possible to search for the longest path that starts within an area. In order to do that, click the
|
||||
"Define the search area for the longest route" button with the 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="M128 32l32 0c17.7 0 32 14.3 32 32l0 32L96 96l0-32c0-17.7 14.3-32 32-32zm64 96l0 320c0 17.7-14.3 32-32 32L32 480c-17.7 0-32-14.3-32-32l0-59.1c0-34.6 9.4-68.6 27.2-98.3C40.9 267.8 49.7 242.4 53 216L60.5 156c2-16 15.6-28 31.8-28l99.8 0zm227.8 0c16.1 0 29.8 12 31.8 28L459 216c3.3 26.4 12.1 51.8 25.8 74.6c17.8 29.7 27.2 63.7 27.2 98.3l0 59.1c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-320 99.8 0zM320 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 32-96 0 0-32zm-32 64l0 160-64 0 0-160 64 0z"/></svg>
|
||||
at the top left of the map. After you've drawn the search area on the map, click the "Start search" button.
|
||||
A table will show a running tally of the longest routes found.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
798
native/route_search.cpp
Normal file
@ -0,0 +1,798 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <arpa/inet.h>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
// Constants
|
||||
const float GRAVITY_ACCELERATION = 9.81;
|
||||
|
||||
|
||||
// Data structures
|
||||
struct Connection {
|
||||
int connected_point_number;
|
||||
float distance;
|
||||
uint8_t speed_limit;
|
||||
bool motorway;
|
||||
bool tunnel;
|
||||
bool againstOneWay;
|
||||
};
|
||||
|
||||
struct RoadNode{
|
||||
float position_x;
|
||||
float position_y;
|
||||
float position_z;
|
||||
|
||||
Connection connection_one;
|
||||
Connection connection_two;
|
||||
std::vector<Connection> *extra_connections;
|
||||
};
|
||||
|
||||
struct RoadNodeSet {
|
||||
uint32_t numberOfNodes;
|
||||
RoadNode* roadNodes;
|
||||
};
|
||||
|
||||
struct SearchNodeInfo {
|
||||
float distanceFromPrevious;
|
||||
float currentSpeed;
|
||||
};
|
||||
|
||||
struct SearchResult {
|
||||
uint32_t startingNode;
|
||||
std::map<uint32_t, uint32_t> previous;
|
||||
std::map<uint32_t, SearchNodeInfo> reachableNodes;
|
||||
};
|
||||
|
||||
struct JSNodeInfo {
|
||||
uint32_t nodeId;
|
||||
float positionX;
|
||||
float positionY;
|
||||
float positionZ;
|
||||
float distanceFromStart;
|
||||
float currentSpeed;
|
||||
};
|
||||
|
||||
struct JSSearchResult {
|
||||
std::vector<JSNodeInfo> endPoints;
|
||||
};
|
||||
|
||||
struct ListNode {
|
||||
int id;
|
||||
float current_speed;
|
||||
|
||||
ListNode* next = NULL;
|
||||
};
|
||||
|
||||
struct PolygonCoordinate {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct AreaSearchEntry {
|
||||
uint32_t nodeId;
|
||||
float positionX;
|
||||
float positionY;
|
||||
float longestRoute;
|
||||
};
|
||||
|
||||
struct AreaSearchResult {
|
||||
uint32_t remainingNodes;
|
||||
std::vector<AreaSearchEntry> searchedNodes;
|
||||
};
|
||||
|
||||
struct CurrentAreaSearch {
|
||||
float minimumSpeed;
|
||||
float maximumSpeed;
|
||||
float maximumSpeedLimit;
|
||||
float dragCoefficient;
|
||||
bool allowMotorways;
|
||||
bool allowTunnels;
|
||||
bool allowAgainstOneway;
|
||||
|
||||
std::vector<uint32_t> startNodes;
|
||||
size_t startNodeCounter = 0;
|
||||
std::vector<AreaSearchEntry> currentAreaSearchResults;
|
||||
std::set<uint32_t> utilizedNodes;
|
||||
};
|
||||
|
||||
// Data
|
||||
RoadNodeSet set;
|
||||
SearchResult lastSearchResult;
|
||||
std::set<uint32_t> excludedNodes;
|
||||
|
||||
CurrentAreaSearch currentAreaSearch;
|
||||
|
||||
// Data loading functions
|
||||
float getFloatFromBuffer(char* buffer) {
|
||||
// First, cast the buffer to an int, in order to reverse bytes from network order to host order
|
||||
uint32_t correctByteOrder = ntohl(*reinterpret_cast<uint32_t*>(buffer));
|
||||
return *((float*) &correctByteOrder);
|
||||
}
|
||||
|
||||
void addLinkToRoadNode(RoadNode* roadNodes, int node_from, int node_to, uint8_t speed_limit, bool motorway, bool tunnel, bool againstOneWay) {
|
||||
float from_x = roadNodes[node_from].position_x;
|
||||
float from_y = roadNodes[node_from].position_y;
|
||||
float to_x = roadNodes[node_to].position_x;
|
||||
float to_y = roadNodes[node_to].position_y;
|
||||
|
||||
float distance = sqrt(pow(to_x - from_x, 2) + pow(to_y - from_y, 2));
|
||||
|
||||
Connection connection;
|
||||
connection.connected_point_number = node_to;
|
||||
connection.distance = distance;
|
||||
connection.speed_limit = speed_limit;
|
||||
connection.motorway = motorway;
|
||||
connection.tunnel = tunnel;
|
||||
connection.againstOneWay = againstOneWay;
|
||||
|
||||
if (roadNodes[node_from].connection_one.connected_point_number == -1) {
|
||||
roadNodes[node_from].connection_one = connection;
|
||||
} else if (roadNodes[node_from].connection_two.connected_point_number == -1) {
|
||||
roadNodes[node_from].connection_two = connection;
|
||||
} else {
|
||||
if (roadNodes[node_from].extra_connections == NULL) {
|
||||
roadNodes[node_from].extra_connections = new std::vector<Connection>();
|
||||
}
|
||||
|
||||
roadNodes[node_from].extra_connections->push_back(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void loadData(std::string filePath) {
|
||||
set = RoadNodeSet();
|
||||
|
||||
std::ifstream source(filePath.c_str(), std::ios_base::binary);
|
||||
char *buffer = new char[4];
|
||||
source.read(buffer, 4);
|
||||
uint32_t number_of_entries = ntohl(*reinterpret_cast<uint32_t*>(buffer));
|
||||
std::cout << "Reading " << number_of_entries << " road nodes" << std::endl;
|
||||
|
||||
// Create the memory space for all these road nodes
|
||||
set.numberOfNodes = number_of_entries;
|
||||
set.roadNodes = new RoadNode[number_of_entries];
|
||||
for (size_t i = 0; i < number_of_entries; i++) {
|
||||
// Each node in the file is of the type float x, float y, short z
|
||||
source.read(buffer, 4);
|
||||
// First, cast this to an int, reverse the byte order, and then cast to float
|
||||
float position_x = getFloatFromBuffer(buffer);
|
||||
source.read(buffer, 4);
|
||||
float position_y = getFloatFromBuffer(buffer);
|
||||
source.read(buffer, 2);
|
||||
int position_z_int = ntohs(*reinterpret_cast<uint16_t*>(buffer));
|
||||
float position_z = (position_z_int - 30000) / 10.0;
|
||||
|
||||
set.roadNodes[i].position_x = position_x;
|
||||
set.roadNodes[i].position_y = position_y;
|
||||
set.roadNodes[i].position_z = position_z;
|
||||
|
||||
set.roadNodes[i].connection_one.connected_point_number = -1;
|
||||
set.roadNodes[i].connection_two.connected_point_number = -1;
|
||||
|
||||
set.roadNodes[i].extra_connections = NULL;
|
||||
}
|
||||
|
||||
source.read(buffer, 4);
|
||||
uint32_t number_of_links = ntohl(*reinterpret_cast<uint32_t*>(buffer));
|
||||
std::cout << "Reading " << number_of_links << " links" << std::endl;
|
||||
|
||||
// Read all the links between nodes
|
||||
int connection_vectors = 0;
|
||||
for (size_t i = 0; i < number_of_links; i++) {
|
||||
source.read(buffer, 4);
|
||||
uint32_t from_point = ntohl(*reinterpret_cast<uint32_t*>(buffer));
|
||||
source.read(buffer, 4);
|
||||
uint32_t to_point = ntohl(*reinterpret_cast<uint32_t*>(buffer));
|
||||
|
||||
source.read(buffer, 1);
|
||||
uint8_t flags_byte = *reinterpret_cast<uint8_t*>(buffer);
|
||||
|
||||
uint8_t speed_limit = (flags_byte >> 4) * 10;
|
||||
|
||||
bool motorway = (flags_byte & 0x01) > 0;
|
||||
bool tunnel = (flags_byte & 0x02) > 0;
|
||||
bool passable_same_direction = (flags_byte & 0x04) > 0;
|
||||
bool passable_opposite_direction = (flags_byte & 0x08) > 0;
|
||||
|
||||
addLinkToRoadNode(set.roadNodes, from_point, to_point, speed_limit, motorway, tunnel, !passable_same_direction);
|
||||
addLinkToRoadNode(set.roadNodes, to_point, from_point, speed_limit, motorway, tunnel, !passable_opposite_direction);
|
||||
}
|
||||
|
||||
delete[] buffer;
|
||||
}
|
||||
|
||||
// Search functions
|
||||
JSNodeInfo findClosestNode(float position_x, float position_y) {
|
||||
float closestDistance = 1e99;
|
||||
uint32_t closestNode = 0;
|
||||
|
||||
for (size_t i = 0; i < set.numberOfNodes; i++) {
|
||||
RoadNode node = set.roadNodes[i];
|
||||
float node_x = node.position_x;
|
||||
float node_y = node.position_y;
|
||||
|
||||
float distance = sqrt(pow(position_x - node_x, 2) + pow(position_y - node_y, 2));
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestNode = i;
|
||||
}
|
||||
}
|
||||
|
||||
JSNodeInfo result;
|
||||
result.nodeId = closestNode;
|
||||
result.positionX = set.roadNodes[closestNode].position_x;
|
||||
result.positionY = set.roadNodes[closestNode].position_y;
|
||||
result.positionZ = set.roadNodes[closestNode].position_z;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
float calculate_speed(float starting_speed, float horizontal_distance, float height_difference, float minimum_speed, float maximum_speed, float drag_coefficient) {
|
||||
float slope_tan = height_difference / horizontal_distance;
|
||||
float final_speed = -1;
|
||||
|
||||
// If the slope is flat, that is one calculation
|
||||
if (fabs(slope_tan) < 0.0001) {
|
||||
float time_to_finish = (exp(horizontal_distance * drag_coefficient) - 1) / (starting_speed * drag_coefficient);
|
||||
final_speed = starting_speed / (starting_speed * drag_coefficient * time_to_finish + 1);
|
||||
} else {
|
||||
// Otherwise, we need to find some parameters
|
||||
float slope = atan(slope_tan);
|
||||
float slope_sin = sin(slope);
|
||||
float full_distance = horizontal_distance * slope_tan / slope_sin;
|
||||
float acceleration = -GRAVITY_ACCELERATION * slope_sin;
|
||||
float terminal_velocity = sqrt(fabs(acceleration) / drag_coefficient);
|
||||
|
||||
// Uphill
|
||||
if (slope > 0) {
|
||||
float time_to_peak = atan(starting_speed / terminal_velocity) / (drag_coefficient * terminal_velocity);
|
||||
// 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(drag_coefficient * terminal_velocity * time_to_peak) * exp(full_distance * drag_coefficient);
|
||||
if (discriminant > 1.f) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
float time_to_reach_end = time_to_peak - acos(discriminant) / (drag_coefficient * terminal_velocity);
|
||||
final_speed = terminal_velocity * tan(drag_coefficient * terminal_velocity * (time_to_peak - time_to_reach_end));
|
||||
} else {
|
||||
// Downhill
|
||||
// If the starting speed is very close to the terminal velocity, we'll just stay at terminal velocity
|
||||
if (fabs(starting_speed - terminal_velocity) < 0.001) {
|
||||
final_speed = terminal_velocity;
|
||||
} else if (starting_speed < terminal_velocity) {
|
||||
float k1 = terminal_velocity * log((terminal_velocity + starting_speed) / (terminal_velocity - starting_speed)) * 0.5;
|
||||
float k2 = -log(cosh(k1 / terminal_velocity)) / drag_coefficient;
|
||||
float time_spent = acosh(exp(drag_coefficient * (full_distance - k2))) / (drag_coefficient * terminal_velocity) - k1 / (drag_coefficient * pow(terminal_velocity, 2));
|
||||
final_speed = terminal_velocity * tanh(drag_coefficient * terminal_velocity * time_spent + k1 / terminal_velocity);
|
||||
} else if (starting_speed > terminal_velocity) {
|
||||
float k1 = log((starting_speed - terminal_velocity) / (starting_speed + terminal_velocity)) * terminal_velocity / 2;
|
||||
float k2 = -log(-sinh(k1 / terminal_velocity)) / drag_coefficient;
|
||||
float time_spent = k1 / (drag_coefficient * pow(terminal_velocity, 2)) - asinh(-exp(drag_coefficient * (full_distance - k2))) / (drag_coefficient * terminal_velocity);
|
||||
final_speed = -terminal_velocity / tanh(k1 / terminal_velocity - drag_coefficient * terminal_velocity * time_spent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (final_speed < minimum_speed) {
|
||||
return -1;
|
||||
} else {
|
||||
return std::min(final_speed, maximum_speed);
|
||||
}
|
||||
}
|
||||
|
||||
void getNeighbourConnections(RoadNode node, Connection* targetArray, int &numberOfConnections) {
|
||||
numberOfConnections = 0;
|
||||
if (node.connection_one.connected_point_number != -1) {
|
||||
*(targetArray + numberOfConnections) = node.connection_one;
|
||||
numberOfConnections++;
|
||||
}
|
||||
if (node.connection_two.connected_point_number != -1) {
|
||||
*(targetArray + numberOfConnections) = node.connection_two;
|
||||
numberOfConnections++;
|
||||
}
|
||||
if (node.extra_connections != NULL) {
|
||||
for (auto it = node.extra_connections->begin(); it != node.extra_connections->end(); it++) {
|
||||
*(targetArray + numberOfConnections) = *it;
|
||||
numberOfConnections++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchResult findAllPathsFromPoint(int startingNode, float minimum_speed, float maximum_speed, int maximumSpeedLimit, float drag_coefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway) {
|
||||
SearchResult result;
|
||||
result.startingNode = startingNode;
|
||||
|
||||
RoadNode firstNode = set.roadNodes[startingNode];
|
||||
SearchNodeInfo firstNodeInfo;
|
||||
firstNodeInfo.distanceFromPrevious = 0;
|
||||
firstNodeInfo.currentSpeed = minimum_speed;
|
||||
result.reachableNodes[startingNode] = firstNodeInfo;
|
||||
|
||||
ListNode *nextNode = new ListNode;
|
||||
ListNode *lastNode = nextNode;
|
||||
|
||||
nextNode->id = startingNode;
|
||||
nextNode->current_speed = minimum_speed;
|
||||
|
||||
while (nextNode != NULL) {
|
||||
ListNode *currentNode = nextNode;
|
||||
nextNode = currentNode->next;
|
||||
|
||||
int currentId = currentNode->id;
|
||||
float currentSpeed = currentNode->current_speed;
|
||||
|
||||
RoadNode bestNode = set.roadNodes[currentId];
|
||||
|
||||
if (lastNode == currentNode) {
|
||||
lastNode = NULL;
|
||||
}
|
||||
delete currentNode;
|
||||
|
||||
Connection neighbours[10];
|
||||
int neighbourCounter = 0;
|
||||
getNeighbourConnections(bestNode, neighbours, neighbourCounter);
|
||||
|
||||
for (int i = 0; i < neighbourCounter; i++) {
|
||||
Connection neighbour = neighbours[i];
|
||||
// First, if neighbour is in excluded area, skip it
|
||||
if (excludedNodes.find(neighbour.connected_point_number) != excludedNodes.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbour.speed_limit > maximumSpeedLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbour.motorway && !allowMotorways) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbour.tunnel && !allowTunnels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbour.againstOneWay && ! allowAgainstOneway) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RoadNode neighbourNode = set.roadNodes[neighbour.connected_point_number];
|
||||
float heightDifference = neighbourNode.position_z - bestNode.position_z;
|
||||
float resultingSpeed = calculate_speed(currentSpeed, neighbour.distance, heightDifference, minimum_speed, maximum_speed, drag_coefficient);
|
||||
|
||||
if (resultingSpeed < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this node is already in the reachable nodes map
|
||||
auto resultIterator = result.reachableNodes.find(neighbour.connected_point_number);
|
||||
if (resultIterator != result.reachableNodes.end() && resultingSpeed <= resultIterator->second.currentSpeed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SearchNodeInfo reachableNodeInfo;
|
||||
reachableNodeInfo.currentSpeed = resultingSpeed;
|
||||
reachableNodeInfo.distanceFromPrevious = neighbour.distance;
|
||||
result.reachableNodes[neighbour.connected_point_number] = reachableNodeInfo;
|
||||
|
||||
result.previous[neighbour.connected_point_number] = currentId;
|
||||
|
||||
ListNode *neighbourListNode = new ListNode;
|
||||
neighbourListNode->id = neighbour.connected_point_number;
|
||||
neighbourListNode->current_speed = reachableNodeInfo.currentSpeed;
|
||||
|
||||
if (nextNode == NULL || resultingSpeed < nextNode->current_speed) {
|
||||
neighbourListNode->next = nextNode;
|
||||
nextNode = neighbourListNode;
|
||||
} else {
|
||||
ListNode* previousSearchNode = nextNode;
|
||||
ListNode* currentSearchNode = nextNode->next;
|
||||
|
||||
while(currentSearchNode != NULL && currentSearchNode->current_speed > resultingSpeed) {
|
||||
previousSearchNode = currentSearchNode;
|
||||
currentSearchNode = currentSearchNode->next;
|
||||
}
|
||||
|
||||
previousSearchNode->next = neighbourListNode;
|
||||
neighbourListNode->next = currentSearchNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway) {
|
||||
lastSearchResult = findAllPathsFromPoint(startingNode, minimumSpeed, maximumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway);
|
||||
|
||||
float start_x = set.roadNodes[startingNode].position_x;
|
||||
float start_y = set.roadNodes[startingNode].position_y;
|
||||
|
||||
// Find all end points and sort them by distance
|
||||
std::set<uint32_t> notEndPoints;
|
||||
for (auto it = lastSearchResult.previous.begin(); it != lastSearchResult.previous.end(); it++) {
|
||||
notEndPoints.insert(it->second);
|
||||
}
|
||||
|
||||
std::vector<JSNodeInfo> farthestEndpoints;
|
||||
for (auto it = lastSearchResult.reachableNodes.begin(); it != lastSearchResult.reachableNodes.end(); it++) {
|
||||
if (notEndPoints.find(it->first) == notEndPoints.end()) {
|
||||
JSNodeInfo entry;
|
||||
entry.nodeId = it->first;
|
||||
entry.positionX = set.roadNodes[entry.nodeId].position_x;
|
||||
entry.positionY = set.roadNodes[entry.nodeId].position_y;
|
||||
entry.distanceFromStart = sqrt(pow(entry.positionX - start_x, 2) + pow(entry.positionY - start_y, 2));
|
||||
|
||||
farthestEndpoints.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(farthestEndpoints.begin(), farthestEndpoints.end(), [](JSNodeInfo first, JSNodeInfo second){return second.distanceFromStart < first.distanceFromStart;});
|
||||
|
||||
// Discard all points that are too close to each other
|
||||
float minimumDistance = 300;
|
||||
|
||||
std::vector<JSNodeInfo> filteredEndpoints;
|
||||
for (size_t i = 0; i < farthestEndpoints.size(); i++) {
|
||||
float closestNode = 1e99;
|
||||
JSNodeInfo currentNode = farthestEndpoints.at(i);
|
||||
for (size_t j = 0; j < filteredEndpoints.size(); j++) {
|
||||
JSNodeInfo otherNode = filteredEndpoints.at(j);
|
||||
float distance = sqrt(pow(otherNode.positionX - currentNode.positionX, 2) + pow(otherNode.positionY - currentNode.positionY, 2));
|
||||
if (distance < closestNode) {
|
||||
closestNode = distance;
|
||||
if (distance <= minimumDistance) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closestNode > minimumDistance) {
|
||||
filteredEndpoints.push_back(currentNode);
|
||||
}
|
||||
}
|
||||
|
||||
JSSearchResult searchResult;
|
||||
searchResult.endPoints = filteredEndpoints;
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode) {
|
||||
std::vector<JSNodeInfo> path;
|
||||
|
||||
if (lastSearchResult.startingNode != startingNode) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (lastSearchResult.reachableNodes.find(endNode) == lastSearchResult.reachableNodes.end()) {
|
||||
return path;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> nodes;
|
||||
uint32_t currentNode = endNode;
|
||||
nodes.push_back(currentNode);
|
||||
while (lastSearchResult.previous.find(currentNode) != lastSearchResult.previous.end()) {
|
||||
currentNode = lastSearchResult.previous.find(currentNode)->second;
|
||||
nodes.push_back(currentNode);
|
||||
}
|
||||
|
||||
float currentDistance = 0;
|
||||
|
||||
for (auto it = nodes.rbegin(); it != nodes.rend(); it++) {
|
||||
RoadNode roadNode = set.roadNodes[*it];
|
||||
JSNodeInfo nodeInfo;
|
||||
nodeInfo.nodeId = *it;
|
||||
nodeInfo.positionX = roadNode.position_x;
|
||||
nodeInfo.positionY = roadNode.position_y;
|
||||
nodeInfo.positionZ = roadNode.position_z;
|
||||
|
||||
SearchNodeInfo searchNodeInfo = lastSearchResult.reachableNodes.find(*it)->second;
|
||||
nodeInfo.currentSpeed = searchNodeInfo.currentSpeed;
|
||||
currentDistance += searchNodeInfo.distanceFromPrevious;
|
||||
nodeInfo.distanceFromStart = currentDistance;
|
||||
|
||||
path.push_back(nodeInfo);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
bool isInsideRing(float pointX, float pointY, std::vector<PolygonCoordinate> ring) {
|
||||
// Draw a ray from the point directly towards the right, see if it crosses any line in the ring
|
||||
int crossings = 0;
|
||||
for (int i = 1; i < ring.size() + 1; i++) {
|
||||
int currentPoint = i % ring.size();
|
||||
int lastPoint = i - 1;
|
||||
|
||||
float lastY = ring.at(lastPoint).y;
|
||||
float currentY = ring.at(currentPoint).y;
|
||||
|
||||
if (pointY > fmax(lastY, currentY) || pointY < fmin(lastY, currentY)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float lastX = ring.at(lastPoint).x;
|
||||
float currentX = ring.at(currentPoint).x;
|
||||
|
||||
if (pointX > fmax(lastX, currentX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pointX < fmin(lastX, currentX)) {
|
||||
crossings += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we don't go cleanly through the bounding box, we have to be a bit more smart about this
|
||||
// We don't want to divide by zero. If the line is completely horizontal, we have a freak line which we will ignore
|
||||
if (fabs(lastY - currentY) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float slope = (currentX - lastX) / (currentY - lastY);
|
||||
float xAtPointY = lastX + (pointY - lastY) * slope;
|
||||
|
||||
if (xAtPointY >= pointX) {
|
||||
crossings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return crossings % 2 == 1;
|
||||
}
|
||||
|
||||
void getNodesWithinPolygons(std::vector<std::vector<std::vector<PolygonCoordinate>>> polygons, std::set<uint32_t> &resultSet) {
|
||||
// Add new ones
|
||||
for (auto polygon : polygons) {
|
||||
// Get a bounding box for each polygon
|
||||
float maxX = -1e99;
|
||||
float minX = 1e99;
|
||||
float maxY = -1e99;
|
||||
float minY = 1e99;
|
||||
|
||||
for (auto ring : polygon) {
|
||||
for (auto coordinate : ring) {
|
||||
minX = fmin(minX, coordinate.x);
|
||||
minY = fmin(minY, coordinate.y);
|
||||
maxX = fmax(maxX, coordinate.x);
|
||||
maxY = fmax(maxY, coordinate.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Go through all nodes
|
||||
for (size_t nodeId = 0; nodeId < set.numberOfNodes; nodeId++) {
|
||||
RoadNode node = set.roadNodes[nodeId];
|
||||
// If the node is outside the bounding box, just move on
|
||||
if (node.position_x < minX || node.position_x > maxX || node.position_y < minY || node.position_y > maxY) {
|
||||
continue;
|
||||
}
|
||||
// Otherwise, count how many times a ray straight east from the point crosses the polygon's rings
|
||||
int crossings = 0;
|
||||
for (auto ring : polygon) {
|
||||
if (isInsideRing(node.position_x, node.position_y, ring)) {
|
||||
crossings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (crossings % 2 == 1) {
|
||||
resultSet.insert(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void excludeNodesWithinPolygons(std::vector<std::vector<std::vector<PolygonCoordinate>>> polygons) {
|
||||
// Clear any old excluded nodes
|
||||
excludedNodes.clear();
|
||||
getNodesWithinPolygons(polygons, excludedNodes);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> findPossibleStartNodes(float minimumSpeed, float maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, std::vector<std::vector<std::vector<PolygonCoordinate>>> searchArea) {
|
||||
std::set<uint32_t> allNodesWithinSearchArea;
|
||||
getNodesWithinPolygons(searchArea, allNodesWithinSearchArea);
|
||||
|
||||
float minimumSlope = asin(dragCoefficient * minimumSpeed * minimumSpeed / GRAVITY_ACCELERATION);
|
||||
float minimumSlopeTan = tan(minimumSlope);
|
||||
|
||||
std::vector<uint32_t> possibleStartNodes;
|
||||
for (uint32_t nodeId : allNodesWithinSearchArea) {
|
||||
if (excludedNodes.find(nodeId) != excludedNodes.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasWayOut = false;
|
||||
bool hasWayIn = false;
|
||||
|
||||
RoadNode node = set.roadNodes[nodeId];
|
||||
Connection neighbours[10];
|
||||
int numberOfNeighbours = 0;
|
||||
getNeighbourConnections(node, neighbours, numberOfNeighbours);
|
||||
|
||||
for (int i = 0; i < numberOfNeighbours; i++) {
|
||||
Connection connection = neighbours[i];
|
||||
if (excludedNodes.find(connection.connected_point_number) != excludedNodes.end()) {
|
||||
continue;
|
||||
}
|
||||
if (connection.motorway && !allowMotorways) {
|
||||
continue;
|
||||
}
|
||||
if (connection.tunnel && !allowTunnels) {
|
||||
continue;
|
||||
}
|
||||
if (connection.againstOneWay && !allowAgainstOneway) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RoadNode neighbourNode = set.roadNodes[connection.connected_point_number];
|
||||
float slopeTan = (neighbourNode.position_z - node.position_z) / connection.distance;
|
||||
if (slopeTan > minimumSlopeTan) {
|
||||
hasWayIn = true;
|
||||
break;
|
||||
} else if (slopeTan < -minimumSlopeTan) {
|
||||
hasWayOut = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWayOut && !hasWayIn) {
|
||||
// Insertion sort by height
|
||||
possibleStartNodes.insert(
|
||||
std::upper_bound(
|
||||
possibleStartNodes.begin(),
|
||||
possibleStartNodes.end(),
|
||||
nodeId,
|
||||
[](uint32_t nodeOne, uint32_t nodeTwo){return set.roadNodes[nodeOne].position_z > set.roadNodes[nodeTwo].position_z;}
|
||||
),
|
||||
nodeId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return possibleStartNodes;
|
||||
}
|
||||
|
||||
AreaSearchResult startAreaSearch(float minimumSpeed, float maximumSpeed, float maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, std::vector<std::vector<std::vector<PolygonCoordinate>>> searchArea) {
|
||||
currentAreaSearch = CurrentAreaSearch();
|
||||
currentAreaSearch.startNodes = findPossibleStartNodes(minimumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, searchArea);
|
||||
currentAreaSearch.minimumSpeed = minimumSpeed;
|
||||
currentAreaSearch.maximumSpeed = maximumSpeed;
|
||||
currentAreaSearch.maximumSpeedLimit = maximumSpeedLimit;
|
||||
currentAreaSearch.dragCoefficient = dragCoefficient;
|
||||
currentAreaSearch.allowMotorways = allowMotorways;
|
||||
currentAreaSearch.allowTunnels = allowTunnels;
|
||||
currentAreaSearch.allowAgainstOneway = allowAgainstOneway;
|
||||
|
||||
AreaSearchResult result;
|
||||
result.remainingNodes = currentAreaSearch.startNodes.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
AreaSearchResult continueAreaSearch() {
|
||||
uint32_t currentStartNode = currentAreaSearch.startNodes.at(currentAreaSearch.startNodeCounter);
|
||||
SearchResult result = findAllPathsFromPoint(
|
||||
currentStartNode,
|
||||
currentAreaSearch.minimumSpeed,
|
||||
currentAreaSearch.maximumSpeed,
|
||||
currentAreaSearch.maximumSpeedLimit,
|
||||
currentAreaSearch.dragCoefficient,
|
||||
currentAreaSearch.allowMotorways,
|
||||
currentAreaSearch.allowTunnels,
|
||||
currentAreaSearch.allowAgainstOneway
|
||||
);
|
||||
|
||||
// Remove all nodes we have reached from here as possible future start nodes
|
||||
auto startNodeIterator = currentAreaSearch.startNodes.begin() + currentAreaSearch.startNodeCounter + 1;
|
||||
while (startNodeIterator != currentAreaSearch.startNodes.end()) {
|
||||
uint32_t startNodeId = *startNodeIterator;
|
||||
if (result.reachableNodes.find(startNodeId) != result.reachableNodes.end()) {
|
||||
startNodeIterator = currentAreaSearch.startNodes.erase(startNodeIterator);
|
||||
} else {
|
||||
startNodeIterator++;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the farthest reachable point from our current start point
|
||||
std::set<uint32_t> notEndPoints;
|
||||
for (auto it = result.previous.begin(); it != result.previous.end(); it++) {
|
||||
notEndPoints.insert(it->second);
|
||||
}
|
||||
|
||||
RoadNode startNode = set.roadNodes[currentStartNode];
|
||||
float farthestDistance = 0;
|
||||
uint32_t farthestNode = 0;
|
||||
for (auto it = result.reachableNodes.begin(); it != result.reachableNodes.end(); it++) {
|
||||
if (notEndPoints.find(it->first) == notEndPoints.end()) {
|
||||
RoadNode endNode = set.roadNodes[it->first];
|
||||
float distance = sqrt(pow(endNode.position_x - startNode.position_x, 2) + pow( endNode.position_y - startNode.position_y, 2));
|
||||
if (distance > farthestDistance) {
|
||||
farthestDistance = distance;
|
||||
farthestNode = it->first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::set<uint32_t> path;
|
||||
path.insert(farthestNode);
|
||||
uint32_t currentNode = farthestNode;
|
||||
while (result.previous.find(currentNode) != result.previous.end()) {
|
||||
currentNode = result.previous.find(currentNode)->second;
|
||||
path.insert(currentNode);
|
||||
}
|
||||
|
||||
// Check if our path overlaps too much with already travelled paths
|
||||
std::vector<uint32_t> overlap;
|
||||
std::set_intersection(path.begin(), path.end(), currentAreaSearch.utilizedNodes.begin(), currentAreaSearch.utilizedNodes.end(), std::back_inserter(overlap));
|
||||
if (overlap.size() < 0.5 * path.size()) {
|
||||
AreaSearchEntry searchEntry;
|
||||
searchEntry.nodeId = currentStartNode;
|
||||
searchEntry.positionX = startNode.position_x;
|
||||
searchEntry.positionY = startNode.position_y;
|
||||
searchEntry.longestRoute = farthestDistance;
|
||||
|
||||
currentAreaSearch.currentAreaSearchResults.insert(
|
||||
std::upper_bound(
|
||||
currentAreaSearch.currentAreaSearchResults.begin(),
|
||||
currentAreaSearch.currentAreaSearchResults.end(),
|
||||
searchEntry,
|
||||
[](AreaSearchEntry one, AreaSearchEntry two){return one.longestRoute > two.longestRoute;}
|
||||
),
|
||||
searchEntry
|
||||
);
|
||||
|
||||
for (uint32_t pathNode : path) {
|
||||
currentAreaSearch.utilizedNodes.insert(pathNode);
|
||||
}
|
||||
}
|
||||
|
||||
currentAreaSearch.startNodeCounter++;
|
||||
|
||||
AreaSearchResult searchResult;
|
||||
searchResult.remainingNodes = currentAreaSearch.startNodes.size() - currentAreaSearch.startNodeCounter;
|
||||
searchResult.searchedNodes = currentAreaSearch.currentAreaSearchResults;
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
emscripten::class_<JSNodeInfo>("NodeInfo")
|
||||
.constructor<>()
|
||||
.property("nodeId", &JSNodeInfo::nodeId)
|
||||
.property("positionX", &JSNodeInfo::positionX)
|
||||
.property("positionY", &JSNodeInfo::positionY)
|
||||
.property("distanceFromStart", &JSNodeInfo::distanceFromStart);
|
||||
|
||||
emscripten::class_<JSSearchResult>("SearchResult")
|
||||
.constructor<>()
|
||||
.property("endPoints", &JSSearchResult::endPoints);
|
||||
|
||||
emscripten::class_<PolygonCoordinate>("PolygonCoordinate")
|
||||
.constructor<>()
|
||||
.property("x", &PolygonCoordinate::x)
|
||||
.property("y", &PolygonCoordinate::y);
|
||||
|
||||
emscripten::class_<AreaSearchEntry>("AreaSearchEntry")
|
||||
.constructor<>()
|
||||
.property("nodeId", &AreaSearchEntry::nodeId)
|
||||
.property("positionX", &AreaSearchEntry::positionX)
|
||||
.property("positionY", &AreaSearchEntry::positionY)
|
||||
.property("longestRoute", &AreaSearchEntry::longestRoute);
|
||||
|
||||
emscripten::class_<AreaSearchResult>("AreaSearchResult")
|
||||
.constructor<>()
|
||||
.property("remainingNodes", &AreaSearchResult::remainingNodes)
|
||||
.property("searchedNodes", &AreaSearchResult::searchedNodes);
|
||||
|
||||
emscripten::register_vector<JSNodeInfo>("NodeInfoArray");
|
||||
emscripten::register_vector<PolygonCoordinate>("Ring");
|
||||
emscripten::register_vector<std::vector<PolygonCoordinate>>("Polygon");
|
||||
emscripten::register_vector<std::vector<std::vector<PolygonCoordinate>>>("MultiPolygon");
|
||||
emscripten::register_vector<AreaSearchEntry>("AreaSearchEntries");
|
||||
|
||||
emscripten::function("loadData", &loadData);
|
||||
emscripten::function("findClosestNode", &findClosestNode);
|
||||
emscripten::function("findAllPathsFromPoint", &findAllPathsFromPointJS);
|
||||
emscripten::function("getPath", &getPathJS);
|
||||
emscripten::function("excludeNodesWithinPolygons", &excludeNodesWithinPolygons);
|
||||
emscripten::function("startAreaSearch", &startAreaSearch);
|
||||
emscripten::function("continueAreaSearch", &continueAreaSearch);
|
||||
}
|
||||
1401
package-lock.json
generated
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "freewheeling",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:emscripten": "cd native && emcc -O3 -lembind -sEXPORT_ES6 -sASSERTIONS -sEXPORTED_RUNTIME_METHODS=FS -sALLOW_MEMORY_GROWTH -sMAXIMUM_MEMORY=4294967296 --emit-tsd route_search.d.ts -o route_search.js route_search.cpp",
|
||||
"build:normal": "tsc && vite build",
|
||||
"build": "npm run build:emscripten && npm run build:normal",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geoman-io/leaflet-geoman-free": "^2.18.3",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/leaflet": "^1.9.19",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-extra-markers": "^1.2.2",
|
||||
"proj4": "^2.19.4"
|
||||
}
|
||||
}
|
||||
117
src/interfaces.ts
Normal file
@ -0,0 +1,117 @@
|
||||
// Messages passed to and from the Web Worker
|
||||
export interface Message {
|
||||
loadData?: LoadData,
|
||||
dataLoaded?: DataLoaded
|
||||
findClosestNode?: FindClosestNode,
|
||||
foundClosestNode?: FoundClosestNode,
|
||||
findPathsFromNode?: FindPathsFromNode,
|
||||
foundPathsFromNode?: FoundPathsFromNode,
|
||||
getFullPath?: GetFullPath,
|
||||
returnFullPath?: ReturnFullPath,
|
||||
excludeAreas?: ExcludeAreas,
|
||||
searchArea?: SearchArea,
|
||||
continueSearch?: ContinueSearch,
|
||||
searchAreaResult?: SearchAreaResult,
|
||||
errorMessage?: ErrorMessage
|
||||
}
|
||||
|
||||
interface LoadData {
|
||||
data: Uint8Array
|
||||
}
|
||||
|
||||
interface DataLoaded {
|
||||
|
||||
}
|
||||
|
||||
interface FindClosestNode {
|
||||
latitude: number,
|
||||
longitude: number
|
||||
}
|
||||
|
||||
interface FoundClosestNode {
|
||||
originalLatitude: number,
|
||||
originalLongitude: number,
|
||||
foundLatitude: number,
|
||||
foundLongitude: number,
|
||||
foundNodeId: number
|
||||
}
|
||||
|
||||
interface FindPathsFromNode {
|
||||
nodeId: number,
|
||||
minimumSpeed: number,
|
||||
maximumSpeed: number,
|
||||
maximumSpeedLimit: number,
|
||||
dragCoefficient: number,
|
||||
allowMotorways: boolean,
|
||||
allowTunnels: boolean,
|
||||
allowAgainstOneway: boolean
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
nodeId: number,
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
distanceFromStart: number
|
||||
}
|
||||
|
||||
interface FoundPathsFromNode {
|
||||
nodeId: number,
|
||||
endpoints: Endpoint[]
|
||||
}
|
||||
|
||||
interface GetFullPath {
|
||||
startNodeId: number,
|
||||
endNodeId: number
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
latitude: number,
|
||||
longitude: number
|
||||
}
|
||||
|
||||
interface ReturnFullPath {
|
||||
coordinates: Coordinate[]
|
||||
}
|
||||
|
||||
export interface Ring {
|
||||
coordinates: Coordinate[]
|
||||
}
|
||||
|
||||
export interface Polygon {
|
||||
rings: Ring[]
|
||||
}
|
||||
|
||||
interface ExcludeAreas {
|
||||
polygons: Polygon[]
|
||||
}
|
||||
|
||||
interface SearchArea {
|
||||
polygons: Polygon[],
|
||||
minimumSpeed: number,
|
||||
maximumSpeed: number,
|
||||
maximumSpeedLimit: number,
|
||||
dragCoefficient: number,
|
||||
allowMotorways: boolean,
|
||||
allowTunnels: boolean,
|
||||
allowAgainstOneway: boolean
|
||||
}
|
||||
|
||||
interface ContinueSearch {
|
||||
|
||||
}
|
||||
|
||||
export interface SearchAreaResultEntry {
|
||||
nodeId: number,
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
longestRoute: number
|
||||
}
|
||||
|
||||
interface SearchAreaResult {
|
||||
remainingNodes: number,
|
||||
searchResults: SearchAreaResultEntry[]
|
||||
}
|
||||
|
||||
interface ErrorMessage {
|
||||
error: string
|
||||
}
|
||||
374
src/main.ts
Normal file
@ -0,0 +1,374 @@
|
||||
import './style.css';
|
||||
import './modules/maphandler/maphandler'
|
||||
import MapHandler from './modules/maphandler/maphandler';
|
||||
import type { Message, Polygon } from './interfaces';
|
||||
|
||||
interface WindowState {
|
||||
state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching'
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
minimumSpeed: number,
|
||||
maximumSpeed: number,
|
||||
maximumSpeedLimit: number,
|
||||
dragCoefficient: number,
|
||||
allowMotorways: boolean,
|
||||
allowTunnels: boolean,
|
||||
allowAgainstOneway: boolean,
|
||||
cutoffDistance: number
|
||||
}
|
||||
|
||||
// Default settings values
|
||||
const DEFAULT_MINIMUM_SPEED = 1;
|
||||
const DEFAULT_MAXIMUM_SPEED = 40;
|
||||
const DEFAULT_MAXIMUM_SPEED_LIMIT = 80;
|
||||
const DEFAULT_DRAG_COEFFICIENT = 0.005;
|
||||
const DEFAULT_ALLOW_MOTORWAYS = false;
|
||||
const DEFAULT_ALLOW_TUNNELS = false;
|
||||
const DEFAULT_ALLOW_AGAINST_ONE_WAY = false;
|
||||
const DEFAULT_CUTOFF_DISTANCE = 1000;
|
||||
|
||||
// Set up variables
|
||||
let routeWorker = new Worker(new URL('./modules/worker/worker.ts', import.meta.url), {type: 'module'});
|
||||
var mapHandler: MapHandler | null = null;
|
||||
var currentSearchArea: Polygon[] = [];
|
||||
|
||||
var currentState: WindowState = {state: 'DataNotLoaded'};
|
||||
|
||||
var lastSearchUpdate = Date.now();
|
||||
|
||||
let freewheelingHeader = document.getElementById('freewheeling-header');
|
||||
let notLoadedContainer = document.getElementById('notloadedcontainer');
|
||||
let loadingContainer = document.getElementById('loadingcontainer');
|
||||
let mapContainer = document.getElementById('mapcontainer');
|
||||
let searchButton = document.getElementById('search-button');
|
||||
let searchStatusParagraph = document.getElementById('search-status-paragraph');
|
||||
let searchResultsTable = document.getElementById('search-result-table');
|
||||
let searchResultTableBody = document.getElementById('search-result-table-body');
|
||||
let settingsButton = document.getElementById('settings-button');
|
||||
let settingsDiv = document.getElementById('settings-div');
|
||||
|
||||
let minimumSpeedInput = document.getElementById('minimum-speed-input');
|
||||
let maximumSpeedInput = document.getElementById('maximum-speed-input');
|
||||
let maximumSpeedLimitInput = document.getElementById('maximum-speed-limit-input');
|
||||
let dragCoefficientInput = document.getElementById('drag-coefficient-input');
|
||||
let allowMotorwaysInput = document.getElementById('allow-motorways-input');
|
||||
let allowTunnelsInput = document.getElementById('allow-tunnels-input');
|
||||
let allowAgainstOnewayInput = document.getElementById('allow-wrong-way-input');
|
||||
let cutoffDistanceInput = document.getElementById('cutoff-distance-input');
|
||||
|
||||
|
||||
// Set up the web worker and what to do when we get messages from it
|
||||
routeWorker.onmessage = e => {
|
||||
let message = e.data as Message;
|
||||
if (message.dataLoaded != null) {
|
||||
setState({state: 'Ready'})
|
||||
}
|
||||
|
||||
else if (message.foundClosestNode != null) {
|
||||
mapHandler?.drawStartNode(message.foundClosestNode.foundNodeId, message.foundClosestNode.foundLatitude, message.foundClosestNode.foundLongitude);
|
||||
let settings = getSettings();
|
||||
let findPathsMessage: Message = {findPathsFromNode: {
|
||||
nodeId: message.foundClosestNode.foundNodeId,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}};
|
||||
routeWorker.postMessage(findPathsMessage);
|
||||
} else if (message.foundPathsFromNode != null) {
|
||||
mapHandler?.drawEndPoints(message.foundPathsFromNode.endpoints);
|
||||
} else if (message.returnFullPath != null) {
|
||||
mapHandler?.drawPath(message.returnFullPath.coordinates);
|
||||
} else if (message.searchAreaResult != null) {
|
||||
searchStatusParagraph?.setHTMLUnsafe('Searching. ' + message.searchAreaResult.remainingNodes + ' possible starting points remain.');
|
||||
let currentTime = Date.now();
|
||||
let settings = getSettings();
|
||||
if (message.searchAreaResult.searchResults.length > 0 && (currentTime - lastSearchUpdate > 500 || message.searchAreaResult.remainingNodes == 0)) {
|
||||
lastSearchUpdate = currentTime;
|
||||
searchResultsTable?.style.setProperty('display', 'block');
|
||||
searchResultTableBody?.setHTMLUnsafe('');
|
||||
|
||||
message.searchAreaResult.searchResults.forEach(result => {
|
||||
if (result.longestRoute < settings.cutoffDistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tableRow = document.createElement('tr');
|
||||
let distanceCell = document.createElement('td');
|
||||
let latitudeCell = document.createElement('td');
|
||||
let longitudeCell = document.createElement('td')
|
||||
let buttonCell = document.createElement('td');
|
||||
searchResultTableBody?.appendChild(tableRow);
|
||||
|
||||
tableRow.appendChild(distanceCell);
|
||||
tableRow.appendChild(latitudeCell);
|
||||
tableRow.appendChild(longitudeCell);
|
||||
tableRow.appendChild(buttonCell);
|
||||
|
||||
distanceCell.setHTMLUnsafe(result.longestRoute.toFixed(0) + ' m');
|
||||
latitudeCell.setHTMLUnsafe(result.latitude.toFixed(6));
|
||||
longitudeCell.setHTMLUnsafe(result.longitude.toFixed(6));
|
||||
|
||||
let button = document.createElement('button');
|
||||
buttonCell.appendChild(button);
|
||||
|
||||
button.setHTMLUnsafe('Show in map');
|
||||
button.addEventListener('click', _ => {
|
||||
mapHandler?.drawStartNode(result.nodeId, result.latitude, result.longitude);
|
||||
let settings = getSettings();
|
||||
let requestMessage: Message = {
|
||||
findPathsFromNode: {
|
||||
nodeId: result.nodeId,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}
|
||||
};
|
||||
routeWorker.postMessage(requestMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (currentState.state == 'Searching' && message.searchAreaResult.remainingNodes > 0) {
|
||||
let continueMessage: Message = {continueSearch: {}};
|
||||
routeWorker.postMessage(continueMessage);
|
||||
} else {
|
||||
searchStatusParagraph?.setHTMLUnsafe('Finished searching.');
|
||||
setState({state: 'Ready'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routeWorker.onerror = e => {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
function setUpMapHandler() {
|
||||
mapHandler = new MapHandler();
|
||||
// Next, set up what to do when the user clicks different stuff in the map
|
||||
mapHandler.addClickedMapListener((latitude, longitude) => {
|
||||
let message: Message = {findClosestNode: {latitude: latitude, longitude: longitude}};
|
||||
routeWorker.postMessage(message);
|
||||
});
|
||||
mapHandler.addClickedEndpointListener((startNodeId, endNodeId) => {
|
||||
let message: Message = {getFullPath: {startNodeId: startNodeId, endNodeId: endNodeId}};
|
||||
routeWorker.postMessage(message);
|
||||
});
|
||||
mapHandler.addExclusionAreaListener(polygons => {
|
||||
let message: Message = {excludeAreas: { polygons: polygons}};
|
||||
routeWorker.postMessage(message);
|
||||
let currentStartPoint = mapHandler?.getCurrentStartPointId();
|
||||
if (currentStartPoint == null) {
|
||||
return;
|
||||
}
|
||||
if (currentStartPoint >= 0) {
|
||||
let settings = getSettings();
|
||||
let newRoutesMessage: Message = {findPathsFromNode: {
|
||||
nodeId: currentStartPoint,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}};
|
||||
routeWorker.postMessage(newRoutesMessage);
|
||||
}
|
||||
});
|
||||
|
||||
mapHandler.addSearchAreaListener(polygons => {
|
||||
currentSearchArea = polygons;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// What to do when the window state changes
|
||||
function setState(state: WindowState) {
|
||||
currentState = state;
|
||||
if (state.state == 'DataNotLoaded') {
|
||||
freewheelingHeader?.style.setProperty('display', 'block');
|
||||
notLoadedContainer?.style.setProperty('display', 'block');
|
||||
loadingContainer?.style.setProperty('display', 'none');
|
||||
mapContainer?.style.setProperty('display', 'none');
|
||||
} else if (state.state == 'DataLoading') {
|
||||
freewheelingHeader?.style.setProperty('display', 'block');
|
||||
notLoadedContainer?.style.setProperty('display', 'none');
|
||||
loadingContainer?.style.setProperty('display', 'block');
|
||||
mapContainer?.style.setProperty('display', 'none');
|
||||
} else if (state.state == 'Ready') {
|
||||
freewheelingHeader?.style.setProperty('display', 'none');
|
||||
notLoadedContainer?.style.setProperty('display', 'none');
|
||||
loadingContainer?.style.setProperty('display', 'none');
|
||||
mapContainer?.style.setProperty('display', 'block');
|
||||
if (mapHandler == null) {
|
||||
setUpMapHandler();
|
||||
}
|
||||
mapHandler?.enableEditing();
|
||||
enableSettings();
|
||||
searchButton?.setHTMLUnsafe('Start search');
|
||||
} else if (state.state == 'Searching') {
|
||||
freewheelingHeader?.style.setProperty('display', 'none');
|
||||
notLoadedContainer?.style.setProperty('display', 'none');
|
||||
loadingContainer?.style.setProperty('display', 'none');
|
||||
mapContainer?.style.setProperty('display', 'block');
|
||||
if (mapHandler == null) {
|
||||
setUpMapHandler();
|
||||
}
|
||||
mapHandler?.disableEditing();
|
||||
disableSettings();
|
||||
searchButton?.setHTMLUnsafe('Cancel search');
|
||||
}
|
||||
};
|
||||
setState({state: 'DataNotLoaded'});
|
||||
|
||||
// Set up the settings values
|
||||
function setUpNumberInput(element: HTMLElement | null, localStorageKey: string, defaultValue: number): void {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
let numberValue: number = Number(localStorage.getItem(localStorageKey)) || defaultValue;
|
||||
element.value = numberValue.toString();
|
||||
element.addEventListener('change', _ => localStorage.setItem(localStorageKey, element.value));
|
||||
}
|
||||
}
|
||||
|
||||
function setUpBooleanInput(element: HTMLElement | null, localStorageKey: string, defaultValue: boolean): void {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
if (localStorage.getItem(localStorageKey) != null) {
|
||||
element.checked = localStorage.getItem(localStorageKey) === 'true';
|
||||
} else {
|
||||
element.checked = defaultValue;
|
||||
}
|
||||
|
||||
element.addEventListener('change', _ => {localStorage.setItem(localStorageKey, element.checked.toString())});
|
||||
}
|
||||
}
|
||||
|
||||
function getNumberValue(element: HTMLElement | null, defaultValue: number): number {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
return Number(element.value) || defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function getBooleanValue(element: HTMLElement | null, defaultValue: boolean): boolean {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
return element.checked;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function enableInput(element: HTMLElement | null) {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
element.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function disableInput(element: HTMLElement | null) {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
element.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
setUpNumberInput(minimumSpeedInput, 'minimum-speed', DEFAULT_MINIMUM_SPEED);
|
||||
setUpNumberInput(maximumSpeedInput, 'maximum-speed', DEFAULT_MAXIMUM_SPEED);
|
||||
setUpNumberInput(maximumSpeedLimitInput, 'maximum-speed-limit', DEFAULT_MAXIMUM_SPEED_LIMIT);
|
||||
setUpNumberInput(dragCoefficientInput, 'drag-coefficient', DEFAULT_DRAG_COEFFICIENT);
|
||||
setUpBooleanInput(allowMotorwaysInput, 'allow-motorways', DEFAULT_ALLOW_MOTORWAYS);
|
||||
setUpBooleanInput(allowTunnelsInput, 'allow-tunnels', DEFAULT_ALLOW_TUNNELS);
|
||||
setUpBooleanInput(allowAgainstOnewayInput, 'allow-against-one-way', DEFAULT_ALLOW_AGAINST_ONE_WAY);
|
||||
setUpNumberInput(cutoffDistanceInput, 'cutoff-distance', DEFAULT_CUTOFF_DISTANCE);
|
||||
|
||||
function getSettings(): Settings {
|
||||
return {
|
||||
minimumSpeed: getNumberValue(minimumSpeedInput, DEFAULT_MINIMUM_SPEED) / 3.6,
|
||||
maximumSpeed: getNumberValue(maximumSpeedInput, DEFAULT_MAXIMUM_SPEED) / 3.6,
|
||||
maximumSpeedLimit: getNumberValue(maximumSpeedLimitInput, DEFAULT_MAXIMUM_SPEED_LIMIT),
|
||||
dragCoefficient: getNumberValue(dragCoefficientInput, DEFAULT_DRAG_COEFFICIENT),
|
||||
allowMotorways: getBooleanValue(allowMotorwaysInput, DEFAULT_ALLOW_MOTORWAYS),
|
||||
allowTunnels: getBooleanValue(allowTunnelsInput, DEFAULT_ALLOW_TUNNELS),
|
||||
allowAgainstOneway: getBooleanValue(allowAgainstOnewayInput, DEFAULT_ALLOW_AGAINST_ONE_WAY),
|
||||
cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE)
|
||||
};
|
||||
}
|
||||
|
||||
function enableSettings(): void {
|
||||
enableInput(minimumSpeedInput);
|
||||
enableInput(maximumSpeedInput);
|
||||
enableInput(maximumSpeedLimitInput);
|
||||
enableInput(dragCoefficientInput);
|
||||
enableInput(allowMotorwaysInput);
|
||||
enableInput(allowTunnelsInput);
|
||||
enableInput(allowAgainstOnewayInput);
|
||||
}
|
||||
|
||||
function disableSettings(): void {
|
||||
disableInput(minimumSpeedInput);
|
||||
disableInput(maximumSpeedInput);
|
||||
disableInput(maximumSpeedLimitInput);
|
||||
disableInput(dragCoefficientInput);
|
||||
disableInput(allowMotorwaysInput);
|
||||
disableInput(allowTunnelsInput);
|
||||
disableInput(allowAgainstOnewayInput);
|
||||
}
|
||||
|
||||
|
||||
// Finally, set up various events when clicking things
|
||||
searchButton?.addEventListener('click', _ => {
|
||||
if (currentState.state == 'Ready') {
|
||||
if (currentSearchArea.length > 0) {
|
||||
setState({state: 'Searching'})
|
||||
let settings = getSettings();
|
||||
let message: Message = {searchArea: {
|
||||
polygons: currentSearchArea,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}};
|
||||
routeWorker.postMessage(message);
|
||||
}
|
||||
} else if (currentState.state == 'Searching') {
|
||||
setState({state: 'Ready'});
|
||||
}
|
||||
});
|
||||
|
||||
settingsButton?.addEventListener('click', _ => {
|
||||
let settingsDisplay = settingsDiv?.style.getPropertyValue('display');
|
||||
if (settingsDisplay === 'none') {
|
||||
settingsDiv?.style.setProperty('display', 'block');
|
||||
} else {
|
||||
settingsDiv?.style.setProperty('display', 'none');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('data-file-chooser')?.addEventListener('change', chooseEvent => {
|
||||
let eventTarget = chooseEvent.target;
|
||||
if (!(eventTarget instanceof HTMLInputElement) || eventTarget.files == null || eventTarget.files.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({state: 'DataLoading'});
|
||||
let reader = new FileReader();
|
||||
reader.onload = loadEvent => {
|
||||
let data = loadEvent.target?.result;
|
||||
if (data != null && data instanceof ArrayBuffer) {
|
||||
let message: Message = {loadData: {data: new Uint8Array(data)}};
|
||||
routeWorker.postMessage(message);
|
||||
} else {
|
||||
setState({state: 'DataNotLoaded'});
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(eventTarget.files[0]);
|
||||
});
|
||||
93
src/modules/maphandler/icons.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import L from 'leaflet'
|
||||
|
||||
import blueUrl from '../../../static/marker-icon-2x-blue.png'
|
||||
import goldUrl from '../../../static/marker-icon-2x-gold.png'
|
||||
import redUrl from '../../../static/marker-icon-2x-red.png'
|
||||
import greenUrl from '../../../static/marker-icon-2x-green.png'
|
||||
import orangeUrl from '../../../static/marker-icon-2x-orange.png'
|
||||
import yellowUrl from '../../../static/marker-icon-2x-yellow.png'
|
||||
import violetUrl from '../../../static/marker-icon-2x-violet.png'
|
||||
import greyUrl from '../../../static/marker-icon-2x-grey.png'
|
||||
import blackUrl from '../../../static/marker-icon-2x-black.png'
|
||||
import shadowUrl from '../../../static/marker-shadow.png'
|
||||
|
||||
export const blueIcon = new L.Icon({
|
||||
iconUrl: blueUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const goldIcon = new L.Icon({
|
||||
iconUrl: goldUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const redIcon = new L.Icon({
|
||||
iconUrl: redUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const greenIcon = new L.Icon({
|
||||
iconUrl: greenUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const orangeIcon = new L.Icon({
|
||||
iconUrl: orangeUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const yellowIcon = new L.Icon({
|
||||
iconUrl: yellowUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const violetIcon = new L.Icon({
|
||||
iconUrl: violetUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const greyIcon = new L.Icon({
|
||||
iconUrl: greyUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const blackIcon = new L.Icon({
|
||||
iconUrl: blackUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
11
src/modules/maphandler/maphandler.css
Normal file
@ -0,0 +1,11 @@
|
||||
.search-icon {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 32l32 0c17.7 0 32 14.3 32 32l0 32L96 96l0-32c0-17.7 14.3-32 32-32zm64 96l0 320c0 17.7-14.3 32-32 32L32 480c-17.7 0-32-14.3-32-32l0-59.1c0-34.6 9.4-68.6 27.2-98.3C40.9 267.8 49.7 242.4 53 216L60.5 156c2-16 15.6-28 31.8-28l99.8 0zm227.8 0c16.1 0 29.8 12 31.8 28L459 216c3.3 26.4 12.1 51.8 25.8 74.6c17.8 29.7 27.2 63.7 27.2 98.3l0 59.1c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-320 99.8 0zM320 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 32-96 0 0-32zm-32 64l0 160-64 0 0-160 64 0z"/></svg>');
|
||||
}
|
||||
|
||||
.exclude-icon {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!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>');
|
||||
}
|
||||
|
||||
.cancel-icon {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>')
|
||||
}
|
||||
347
src/modules/maphandler/maphandler.ts
Normal file
@ -0,0 +1,347 @@
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L, { FeatureGroup, Marker, Polygon, Polyline, type LatLngTuple } from 'leaflet';
|
||||
import '@geoman-io/leaflet-geoman-free'
|
||||
import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css'
|
||||
|
||||
import './maphandler.css'
|
||||
import { greenIcon, redIcon, violetIcon } from './icons.ts'
|
||||
import type { Endpoint, Coordinate, Polygon as InterfacePolygon } from '../../interfaces.ts'
|
||||
import type { Position } from 'geojson';
|
||||
|
||||
interface ClickedMapListener {
|
||||
(latitude: number, longitude: number): void;
|
||||
}
|
||||
|
||||
interface ClickedEndpointListener {
|
||||
(startpointId: number, endpointId: number): void;
|
||||
}
|
||||
|
||||
interface SearchAreaPolygonListener {
|
||||
(searchAreaPolygons: InterfacePolygon[]): void;
|
||||
}
|
||||
|
||||
interface ExclusionPolygonListener {
|
||||
(exclusionPolygons: InterfacePolygon[]): void;
|
||||
}
|
||||
|
||||
interface OnChangeFunction {
|
||||
(): void;
|
||||
}
|
||||
|
||||
class MapHandler {
|
||||
map;
|
||||
currentStartPoint?: number;
|
||||
|
||||
// Whenever the polygons of either the search area or the exclusion area are changed, these listeners are called
|
||||
clickedMapListeners: ClickedMapListener[] = [];
|
||||
clickedEndpointListeners: ClickedEndpointListener[] = [];
|
||||
searchAreaPolygonListeners: SearchAreaPolygonListener[] = [];
|
||||
exclusionPolygonListeners: ExclusionPolygonListener[] = [];
|
||||
|
||||
// We need feature groups to store the polygons that define these areas
|
||||
searchAreaFeatureGroup: FeatureGroup;
|
||||
exclusionAreaFeatureGroup: FeatureGroup;
|
||||
|
||||
startMarker?: Marker;
|
||||
endMarkers: FeatureGroup;
|
||||
path?: Polyline;
|
||||
|
||||
editingPolygons: boolean = false;
|
||||
|
||||
onChangeFunction?: OnChangeFunction
|
||||
|
||||
constructor() {
|
||||
|
||||
// Use OpenStreetMaps and center on Oslo
|
||||
this.map = L.map('map', {
|
||||
center: L.latLng(59.92, 10.74),
|
||||
zoom: 13,
|
||||
});
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map);
|
||||
|
||||
// Make sure that geoman can only control the polygons it itself has created
|
||||
L.PM.setOptIn(true);
|
||||
this.map.on('pm:create', (e) => {
|
||||
e.layer.options.pmIgnore = false;
|
||||
L.PM.reInitLayer(e.layer);
|
||||
|
||||
if (this.onChangeFunction != null) {
|
||||
e.layer.on('pm:edit', this.onChangeFunction);
|
||||
e.layer.on('pm:drag', this.onChangeFunction);
|
||||
e.layer.on('pm:cut', this.onChangeFunction);
|
||||
e.layer.on('pm:remove', this.onChangeFunction);
|
||||
e.layer.on('pm:rotate', this.onChangeFunction);
|
||||
this.onChangeFunction();
|
||||
};
|
||||
})
|
||||
|
||||
// Make sure we don't start route searches points when editing polygons
|
||||
this.map.on('pm:globaldrawmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globalcutmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globaldragmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globaleditmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globalrotatemodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globalremovalmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
})
|
||||
|
||||
// Add geoman controls for drawing these polygons
|
||||
this.map.pm.addControls({
|
||||
position: 'bottomleft',
|
||||
drawMarker: false,
|
||||
drawCircle: false,
|
||||
drawCircleMarker: false,
|
||||
drawPolyline: false,
|
||||
drawText: false,
|
||||
drawRectangle: false,
|
||||
drawPolygon: false
|
||||
});
|
||||
|
||||
this.map.pm.Toolbar.copyDrawControl('Polygon', {
|
||||
name: 'searcharea',
|
||||
block: 'draw',
|
||||
title: 'Define the search area for longest route',
|
||||
className: 'search-icon',
|
||||
onClick: () => {this.setFeatureGroup(this.searchAreaFeatureGroup, 'green')}
|
||||
})
|
||||
|
||||
this.map.pm.Toolbar.copyDrawControl('Polygon', {
|
||||
name: 'exclusionarea',
|
||||
block: 'draw',
|
||||
title: 'Draw areas that should not be entered',
|
||||
className: 'exclude-icon',
|
||||
onClick: () => {this.setFeatureGroup(this.exclusionAreaFeatureGroup, 'red')}
|
||||
});
|
||||
|
||||
this.map.pm.Toolbar.createCustomControl({
|
||||
name: 'removePolygons',
|
||||
block: 'draw',
|
||||
title: 'Remove all search and exclusion areas',
|
||||
className: 'cancel-icon',
|
||||
onClick: () => {
|
||||
this.searchAreaFeatureGroup.clearLayers();
|
||||
this.exclusionAreaFeatureGroup.clearLayers();
|
||||
this.onExclusionAreaChange();
|
||||
this.onSearchAreaChange();
|
||||
}
|
||||
});
|
||||
|
||||
this.map.pm.Toolbar.setBlockPosition('draw', 'topleft');
|
||||
|
||||
// In order for the polygons in the feature groups to be displayed, they must be added to the map
|
||||
this.searchAreaFeatureGroup = new L.FeatureGroup().addTo(this.map);
|
||||
this.exclusionAreaFeatureGroup = new L.FeatureGroup().addTo(this.map);
|
||||
this.endMarkers = new L.FeatureGroup().addTo(this.map);
|
||||
|
||||
this.map.addEventListener('click', e => {
|
||||
if (!this.editingPolygons) {
|
||||
this.clickedMapListeners.forEach(routeSearchListener => {
|
||||
routeSearchListener(e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.exclusionAreaFeatureGroup.addEventListener('pm:change', _ => {
|
||||
let polygons: Polygon[] = [];
|
||||
this.exclusionAreaFeatureGroup.eachLayer(layer => {
|
||||
if (layer instanceof Polygon) {
|
||||
polygons.push(layer);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getCurrentStartPointId(): number {
|
||||
if (this.currentStartPoint) {
|
||||
return this.currentStartPoint;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private getPolygonsOfFeatureGroup(featureGroup: FeatureGroup): InterfacePolygon[] {
|
||||
let multiPolygonCoordinates: Position[][][] = [];
|
||||
featureGroup.eachLayer(layer => {
|
||||
if (layer instanceof Polygon) {
|
||||
let geojson = layer.toGeoJSON();
|
||||
if (geojson.geometry.type == 'Polygon') {
|
||||
multiPolygonCoordinates.push(geojson.geometry.coordinates);
|
||||
} else if (geojson.geometry.type == 'MultiPolygon') {
|
||||
geojson.geometry.coordinates.forEach(polygon => {
|
||||
multiPolygonCoordinates.push(polygon);
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return multiPolygonCoordinates.map(polygonCoordinates => {
|
||||
return {rings: polygonCoordinates.map(ringCoordinates => {
|
||||
return {coordinates: ringCoordinates.map(singleCoordinate => {
|
||||
return {
|
||||
latitude: singleCoordinate[1],
|
||||
longitude: singleCoordinate[0]
|
||||
}
|
||||
})}
|
||||
})};
|
||||
});
|
||||
}
|
||||
|
||||
private onExclusionAreaChange(): void {
|
||||
let multiPolygon: InterfacePolygon[] = this.getPolygonsOfFeatureGroup(this.exclusionAreaFeatureGroup);
|
||||
this.exclusionPolygonListeners.forEach(listener => {
|
||||
listener(multiPolygon);
|
||||
})
|
||||
}
|
||||
|
||||
private onSearchAreaChange(): void {
|
||||
let multiPolygon: InterfacePolygon[] = this.getPolygonsOfFeatureGroup(this.searchAreaFeatureGroup);
|
||||
this.searchAreaPolygonListeners.forEach(listener => {
|
||||
listener(multiPolygon);
|
||||
})
|
||||
}
|
||||
|
||||
// This function is called when clicking one of the draw buttons, to make sure the polygons drawn have the right colour and
|
||||
// are added to the right feature group
|
||||
private setFeatureGroup(featureGroup: FeatureGroup, color: string): void {
|
||||
this.map.pm.setGlobalOptions({
|
||||
layerGroup: featureGroup,
|
||||
pathOptions: {color: color, fillOpacity: 0.05},
|
||||
templineStyle: {color: color, radius: 10},
|
||||
hintlineStyle: {color: color, dashArray: [5, 5]}
|
||||
});
|
||||
|
||||
if (featureGroup === this.exclusionAreaFeatureGroup) {
|
||||
this.onChangeFunction = this.onExclusionAreaChange;
|
||||
} else if (featureGroup === this.searchAreaFeatureGroup) {
|
||||
this.onChangeFunction = this.onSearchAreaChange;
|
||||
}
|
||||
}
|
||||
|
||||
public addClickedMapListener(routeSearchListener: ClickedMapListener): void {
|
||||
this.clickedMapListeners.push(routeSearchListener);
|
||||
}
|
||||
|
||||
public addClickedEndpointListener(clickedEndpointListener: ClickedEndpointListener): void {
|
||||
this.clickedEndpointListeners.push(clickedEndpointListener);
|
||||
}
|
||||
|
||||
public addExclusionAreaListener(exclusionAreaListener: ExclusionPolygonListener): void {
|
||||
this.exclusionPolygonListeners.push(exclusionAreaListener);
|
||||
}
|
||||
|
||||
public addSearchAreaListener(searchAreaListener: SearchAreaPolygonListener): void {
|
||||
this.searchAreaPolygonListeners.push(searchAreaListener);
|
||||
}
|
||||
|
||||
public drawStartNode(nodeId: number, latitude: number, longitude: number): void {
|
||||
if (this.path != null) {
|
||||
this.map.removeLayer(this.path);
|
||||
this.path = undefined;
|
||||
}
|
||||
|
||||
if (this.startMarker != null) {
|
||||
this.map.removeLayer(this.startMarker);
|
||||
}
|
||||
|
||||
this.currentStartPoint = nodeId;
|
||||
this.endMarkers.clearLayers();
|
||||
this.startMarker = L.marker([latitude, longitude], {icon: greenIcon}).addTo(this.map);
|
||||
}
|
||||
|
||||
public drawEndPoints(endpoints: Endpoint[]): void {
|
||||
this.endMarkers.clearLayers();
|
||||
if (this.path != null) {
|
||||
this.map.removeLayer(this.path);
|
||||
this.path = undefined;
|
||||
}
|
||||
var firstMarker = true;
|
||||
endpoints.forEach(endpoint => {
|
||||
var settings;
|
||||
if (firstMarker) {
|
||||
settings = {
|
||||
icon: redIcon
|
||||
};
|
||||
} else {
|
||||
settings = {
|
||||
icon: violetIcon,
|
||||
opacity: 0.7
|
||||
};
|
||||
}
|
||||
|
||||
var marker = L.marker([endpoint.latitude, endpoint.longitude], settings).addTo(this.endMarkers);
|
||||
marker.bindTooltip(Math.round(endpoint.distanceFromStart) + 'm');
|
||||
if (firstMarker) {
|
||||
marker.openTooltip();
|
||||
}
|
||||
firstMarker = false;
|
||||
|
||||
marker.addEventListener('click', _ => {
|
||||
this.clickedEndpointListeners.forEach(endpointListener => {
|
||||
if (this.currentStartPoint != null) {
|
||||
endpointListener(this.currentStartPoint, endpoint.nodeId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public drawPath(coordinates: Coordinate[]): void {
|
||||
if (this.path != null) {
|
||||
this.map.removeLayer(this.path);
|
||||
}
|
||||
|
||||
var leafletCoordinates = coordinates.map(coordinate => {
|
||||
let latLngTuple: LatLngTuple = [coordinate.latitude, coordinate.longitude];
|
||||
return latLngTuple;
|
||||
});
|
||||
|
||||
this.path = L.polyline(leafletCoordinates).addTo(this.map);
|
||||
}
|
||||
|
||||
public enableEditing() {
|
||||
this.map.pm.Toolbar.setButtonDisabled('searcharea', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('exclusionarea', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('editMode', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('dragMode', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('cutPolygon', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('removalMode', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('rotateMode', false);
|
||||
}
|
||||
|
||||
public disableEditing() {
|
||||
this.map.pm.Toolbar.setButtonDisabled('searcharea', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('exclusionarea', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('editMode', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('dragMode', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('cutPolygon', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('removalMode', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('rotateMode', true);
|
||||
|
||||
this.map.pm.disableDraw();
|
||||
this.map.pm.disableGlobalEditMode();
|
||||
this.map.pm.disableGlobalDragMode();
|
||||
this.map.pm.disableGlobalRemovalMode();
|
||||
this.map.pm.disableGlobalCutMode();
|
||||
this.map.pm.disableGlobalRotateMode();
|
||||
}
|
||||
}
|
||||
|
||||
export default MapHandler;
|
||||
191
src/modules/worker/worker.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import Module, { type AreaSearchEntries, type MainModule, type MultiPolygon } from '../../../native/route_search.js';
|
||||
import proj4 from 'proj4';
|
||||
|
||||
import type { Endpoint, Message, Polygon, SearchAreaResultEntry } from '../../interfaces';
|
||||
|
||||
var dataLoaded = false;
|
||||
var module: MainModule | undefined = undefined;
|
||||
|
||||
function sendErrorMessage(error: string): void {
|
||||
let message: Message = {errorMessage: {error: error}};
|
||||
postMessage(message);
|
||||
}
|
||||
|
||||
function createCMultiPolygon(module: MainModule, polygons: Polygon[]): MultiPolygon {
|
||||
let multiPolygon = new module.MultiPolygon();
|
||||
polygons.forEach(polygon => {
|
||||
let cPolygon = new module.Polygon();
|
||||
polygon.rings.forEach(ring => {
|
||||
let cRing = new module.Ring();
|
||||
ring.coordinates.forEach(coordinate => {
|
||||
let polygonCoordinate = new module.PolygonCoordinate();
|
||||
let utmCoordinates = proj4('EPSG:4326', 'EPSG:32633', [coordinate.longitude, coordinate.latitude]);
|
||||
polygonCoordinate.x = utmCoordinates[0];
|
||||
polygonCoordinate.y = utmCoordinates[1];
|
||||
cRing.push_back(polygonCoordinate);
|
||||
});
|
||||
cPolygon.push_back(cRing);
|
||||
});
|
||||
multiPolygon.push_back(cPolygon);
|
||||
});
|
||||
return multiPolygon;
|
||||
}
|
||||
|
||||
function getAreaSearchResults(searchedNodes: AreaSearchEntries): SearchAreaResultEntry[] {
|
||||
let searchResults: SearchAreaResultEntry[] = [];
|
||||
for (var i = 0; i < searchedNodes.size(); i++) {
|
||||
let searchedNode = searchedNodes.get(i);
|
||||
if (searchedNode != null) {
|
||||
let utmCoordinates = [searchedNode.positionX, searchedNode.positionY];
|
||||
let lngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', utmCoordinates);
|
||||
searchResults.push({
|
||||
nodeId: searchedNode.nodeId,
|
||||
latitude: lngLatCoordinates[1],
|
||||
longitude: lngLatCoordinates[0],
|
||||
longestRoute: searchedNode.longestRoute
|
||||
});
|
||||
}
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
onmessage = async (e) => {
|
||||
if (module == null) {
|
||||
module = await Module();
|
||||
}
|
||||
|
||||
let message = e.data as Message;
|
||||
|
||||
if (message.loadData != null) {
|
||||
module.FS.writeFile('roads.dat', message.loadData.data);
|
||||
module.loadData('roads.dat');
|
||||
dataLoaded = true;
|
||||
|
||||
let returnMessage: Message = {dataLoaded: {}}
|
||||
postMessage(returnMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the data is not loaded, it will not be possible to perform any further operations
|
||||
if (!dataLoaded) {
|
||||
sendErrorMessage('Data not loaded')
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.findClosestNode != null) {
|
||||
let lngLatCoordinates = [message.findClosestNode.longitude, message.findClosestNode.latitude];
|
||||
let utmCoordinates = proj4('EPSG:4326', 'EPSG:32633', lngLatCoordinates);
|
||||
let node = module.findClosestNode(utmCoordinates[0], utmCoordinates[1]);
|
||||
|
||||
let nodeUtmCoordinates = [node.positionX, node.positionY];
|
||||
let nodeLngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', nodeUtmCoordinates);
|
||||
|
||||
let returnMessage: Message = {
|
||||
foundClosestNode: {
|
||||
originalLatitude: message.findClosestNode.latitude,
|
||||
originalLongitude: message.findClosestNode.longitude,
|
||||
foundLatitude: nodeLngLatCoordinates[1],
|
||||
foundLongitude: nodeLngLatCoordinates[0],
|
||||
foundNodeId: node.nodeId
|
||||
}
|
||||
};
|
||||
node.delete();
|
||||
postMessage(returnMessage);
|
||||
};
|
||||
|
||||
if (message.findPathsFromNode != null) {
|
||||
let results = module.findAllPathsFromPoint(
|
||||
message.findPathsFromNode.nodeId,
|
||||
message.findPathsFromNode.minimumSpeed,
|
||||
message.findPathsFromNode.maximumSpeed,
|
||||
message.findPathsFromNode.maximumSpeedLimit,
|
||||
message.findPathsFromNode.dragCoefficient,
|
||||
message.findPathsFromNode.allowMotorways,
|
||||
message.findPathsFromNode.allowTunnels,
|
||||
message.findPathsFromNode.allowAgainstOneway
|
||||
);
|
||||
|
||||
let endpoints: Endpoint[] = [];
|
||||
for (var i = 0; i < results.endPoints.size(); i++) {
|
||||
let nodeData = results.endPoints.get(i);
|
||||
if (!nodeData) {
|
||||
sendErrorMessage('Could not find paths from node ' + message.findPathsFromNode.nodeId);
|
||||
return;
|
||||
}
|
||||
let coordinates = [nodeData.positionX, nodeData.positionY];
|
||||
let lngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', coordinates);
|
||||
endpoints.push({
|
||||
nodeId: nodeData.nodeId,
|
||||
latitude: lngLatCoordinates[1],
|
||||
longitude: lngLatCoordinates[0],
|
||||
distanceFromStart: nodeData.distanceFromStart
|
||||
});
|
||||
}
|
||||
results.delete();
|
||||
|
||||
let returnMessage: Message = {foundPathsFromNode: {nodeId: message.findPathsFromNode.nodeId, endpoints: endpoints}};
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
|
||||
if (message.getFullPath != null) {
|
||||
let path = module.getPath(message.getFullPath.startNodeId, message.getFullPath.endNodeId);
|
||||
if (!path) {
|
||||
sendErrorMessage('Could not get path');
|
||||
return;
|
||||
}
|
||||
var coordinates = [];
|
||||
for (var i = 0; i < path.size(); i++) {
|
||||
let currentPoint = path.get(i);
|
||||
if (!currentPoint) {
|
||||
continue;
|
||||
}
|
||||
coordinates.push([currentPoint.positionX, currentPoint.positionY]);
|
||||
}
|
||||
path.delete();
|
||||
|
||||
coordinates = coordinates.map(utmCoordinate => {
|
||||
let lngLat = proj4('EPSG:32633', 'EPSG:4326', utmCoordinate);
|
||||
return {latitude: lngLat[1], longitude: lngLat[0]};
|
||||
});
|
||||
|
||||
let returnMessage: Message = {returnFullPath: {coordinates: coordinates}};
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
|
||||
if (message.excludeAreas != null) {
|
||||
module.excludeNodesWithinPolygons(createCMultiPolygon(module, message.excludeAreas.polygons));
|
||||
}
|
||||
|
||||
if (message.searchArea != null) {
|
||||
let settings = message.searchArea;
|
||||
let result = module.startAreaSearch(
|
||||
settings.minimumSpeed,
|
||||
settings.maximumSpeed,
|
||||
settings.maximumSpeedLimit,
|
||||
settings.dragCoefficient,
|
||||
settings.allowMotorways,
|
||||
settings.allowTunnels,
|
||||
settings.allowAgainstOneway,
|
||||
createCMultiPolygon(module, settings.polygons)
|
||||
);
|
||||
|
||||
let returnMessage: Message = { searchAreaResult: {
|
||||
remainingNodes: result.remainingNodes,
|
||||
searchResults: getAreaSearchResults(result.searchedNodes)
|
||||
}};
|
||||
|
||||
result.delete();
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
|
||||
if (message.continueSearch != null) {
|
||||
let result = module.continueAreaSearch();
|
||||
let returnMessage: Message = { searchAreaResult: {
|
||||
remainingNodes: result.remainingNodes,
|
||||
searchResults: getAreaSearchResults(result.searchedNodes)
|
||||
}};
|
||||
|
||||
result.delete();
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
}
|
||||
74
src/style.css
Normal file
@ -0,0 +1,74 @@
|
||||
:root {
|
||||
--background-color: #fff7f7;
|
||||
--header-color: #d46a64;
|
||||
--header-stroke-color: #550000;
|
||||
--text-color: #000000;
|
||||
--collapsible-background-color: #AA3939;
|
||||
--collapsible-active-background-color: #D46A6A;
|
||||
--collapsible-color: #FFAAAA;
|
||||
--collapsible-active-color: #550000;
|
||||
--explanation-background-color: #FFAAAA;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
font-family: 'Courier New';
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--header-color);
|
||||
-webkit-text-stroke-width: 2px;
|
||||
-webkit-text-stroke-color: var(--header-stroke-color);
|
||||
font-size: 80px;
|
||||
font-family: 'Helvetica';
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 80vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings-div {
|
||||
width: 40vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
padding: 10px;
|
||||
margin-top:10px;
|
||||
}
|
||||
|
||||
.settings-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
table {
|
||||
width: max(60vw, 800px);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border: solid 1px;
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
th {
|
||||
border: solid 1px;
|
||||
width: max(15vw, 200px);
|
||||
}
|
||||
|
||||
td {
|
||||
border: solid 1px;
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
1
static/bicycle-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M312 32c-13.3 0-24 10.7-24 24s10.7 24 24 24l25.7 0 34.6 64-149.4 0-27.4-38C191 99.7 183.7 96 176 96l-56 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l43.7 0 22.1 30.7-26.6 53.1c-10-2.5-20.5-3.8-31.2-3.8C57.3 224 0 281.3 0 352s57.3 128 128 128c65.3 0 119.1-48.9 127-112l49 0c8.5 0 16.3-4.5 20.7-11.8l84.8-143.5 21.7 40.1C402.4 276.3 384 312 384 352c0 70.7 57.3 128 128 128s128-57.3 128-128s-57.3-128-128-128c-13.5 0-26.5 2.1-38.7 6L375.4 48.8C369.8 38.4 359 32 347.2 32L312 32zM458.6 303.7l32.3 59.7c6.3 11.7 20.9 16 32.5 9.7s16-20.9 9.7-32.5l-32.3-59.7c3.6-.6 7.4-.9 11.2-.9c39.8 0 72 32.2 72 72s-32.2 72-72 72s-72-32.2-72-72c0-18.6 7-35.5 18.6-48.3zM133.2 368l65 0c-7.3 32.1-36 56-70.2 56c-39.8 0-72-32.2-72-72s32.2-72 72-72c1.7 0 3.4 .1 5.1 .2l-24.2 48.5c-9 18.1 4.1 39.4 24.3 39.4zm33.7-48l50.7-101.3 72.9 101.2-.1 .1-123.5 0zm90.6-128l108.5 0L317 274.8 257.4 192z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/marker-icon-2x-black.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/marker-icon-2x-blue.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
static/marker-icon-2x-gold.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/marker-icon-2x-green.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/marker-icon-2x-grey.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/marker-icon-2x-orange.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/marker-icon-2x-red.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/marker-icon-2x-violet.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/marker-icon-2x-yellow.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/marker-shadow.png
Normal file
|
After Width: | Height: | Size: 608 B |
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||