diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dc90e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +native/* +node_modules/ +!native/route_search.cpp diff --git a/README.md b/README.md index 0de05c7..b0b2942 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # Freewheeling -A website for calculating how far you can ride your bike without pedalling \ No newline at end of file +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 \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e59f14f --- /dev/null +++ b/index.html @@ -0,0 +1,92 @@ + + + + + + + Freewheeling + + + +

Freewheeling

+
+

+ 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 Kartkatalogen at Geonorge. + Since I don't have a very powerful server, all calculations are done locally in your browser. +

+

+ 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 + here. 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. +

+ +
+
+

Loading data...

+

This might take a few seconds.

+
+
+
+
+ +

+ + + + + + + + + +
+ + +

Instructions:

+

+ 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. +

+

+ 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: . + Any routes calculated will then avoid the areas drawn. +

+

+ 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 + 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. +

+
+ + diff --git a/native/route_search.cpp b/native/route_search.cpp new file mode 100644 index 0000000..63d7cb2 --- /dev/null +++ b/native/route_search.cpp @@ -0,0 +1,798 @@ +#include +#include + +#include +#include +#include +#include +#include + +// 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 *extra_connections; +}; + +struct RoadNodeSet { + uint32_t numberOfNodes; + RoadNode* roadNodes; +}; + +struct SearchNodeInfo { + float distanceFromPrevious; + float currentSpeed; +}; + +struct SearchResult { + uint32_t startingNode; + std::map previous; + std::map reachableNodes; +}; + +struct JSNodeInfo { + uint32_t nodeId; + float positionX; + float positionY; + float positionZ; + float distanceFromStart; + float currentSpeed; +}; + +struct JSSearchResult { + std::vector 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 searchedNodes; +}; + +struct CurrentAreaSearch { + float minimumSpeed; + float maximumSpeed; + float maximumSpeedLimit; + float dragCoefficient; + bool allowMotorways; + bool allowTunnels; + bool allowAgainstOneway; + + std::vector startNodes; + size_t startNodeCounter = 0; + std::vector currentAreaSearchResults; + std::set utilizedNodes; +}; + +// Data +RoadNodeSet set; +SearchResult lastSearchResult; +std::set 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(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(); + } + + 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(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(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(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(buffer)); + source.read(buffer, 4); + uint32_t to_point = ntohl(*reinterpret_cast(buffer)); + + source.read(buffer, 1); + uint8_t flags_byte = *reinterpret_cast(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 notEndPoints; + for (auto it = lastSearchResult.previous.begin(); it != lastSearchResult.previous.end(); it++) { + notEndPoints.insert(it->second); + } + + std::vector 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 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 getPathJS(uint32_t startingNode, uint32_t endNode) { + std::vector path; + + if (lastSearchResult.startingNode != startingNode) { + return path; + } + + if (lastSearchResult.reachableNodes.find(endNode) == lastSearchResult.reachableNodes.end()) { + return path; + } + + std::vector 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 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>> polygons, std::set &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>> polygons) { + // Clear any old excluded nodes + excludedNodes.clear(); + getNodesWithinPolygons(polygons, excludedNodes); +} + +std::vector findPossibleStartNodes(float minimumSpeed, float maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, std::vector>> searchArea) { + std::set allNodesWithinSearchArea; + getNodesWithinPolygons(searchArea, allNodesWithinSearchArea); + + float minimumSlope = asin(dragCoefficient * minimumSpeed * minimumSpeed / GRAVITY_ACCELERATION); + float minimumSlopeTan = tan(minimumSlope); + + std::vector 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>> 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 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 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 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_("NodeInfo") + .constructor<>() + .property("nodeId", &JSNodeInfo::nodeId) + .property("positionX", &JSNodeInfo::positionX) + .property("positionY", &JSNodeInfo::positionY) + .property("distanceFromStart", &JSNodeInfo::distanceFromStart); + + emscripten::class_("SearchResult") + .constructor<>() + .property("endPoints", &JSSearchResult::endPoints); + + emscripten::class_("PolygonCoordinate") + .constructor<>() + .property("x", &PolygonCoordinate::x) + .property("y", &PolygonCoordinate::y); + + emscripten::class_("AreaSearchEntry") + .constructor<>() + .property("nodeId", &AreaSearchEntry::nodeId) + .property("positionX", &AreaSearchEntry::positionX) + .property("positionY", &AreaSearchEntry::positionY) + .property("longestRoute", &AreaSearchEntry::longestRoute); + + emscripten::class_("AreaSearchResult") + .constructor<>() + .property("remainingNodes", &AreaSearchResult::remainingNodes) + .property("searchedNodes", &AreaSearchResult::searchedNodes); + + emscripten::register_vector("NodeInfoArray"); + emscripten::register_vector("Ring"); + emscripten::register_vector>("Polygon"); + emscripten::register_vector>>("MultiPolygon"); + emscripten::register_vector("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); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..df0144f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1401 @@ +{ + "name": "freewheeling", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "freewheeling", + "version": "0.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" + }, + "devDependencies": { + "typescript": "~5.8.3", + "vite": "^7.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@geoman-io/leaflet-geoman-free": { + "version": "2.18.3", + "resolved": "https://registry.npmjs.org/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.18.3.tgz", + "integrity": "sha512-XzxSKRk2UJUVeGiOt1jU2hyo412Qee1Q0Xsfw4A2r8EoUIo48XKSWfusYe7E53fSPr0aYgZxPevnFdcUXimpdA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-contains": "^6.5.0", + "@turf/kinks": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/line-split": "^6.5.0", + "lodash": "4.17.21", + "polyclip-ts": "^0.16.5" + }, + "peerDependencies": { + "leaflet": "^1.2.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", + "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", + "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", + "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", + "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", + "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", + "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", + "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", + "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", + "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", + "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", + "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", + "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", + "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", + "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", + "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", + "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", + "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", + "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", + "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", + "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turf/bbox": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.5.0.tgz", + "integrity": "sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bearing": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bearing/-/bearing-6.5.0.tgz", + "integrity": "sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-contains": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz", + "integrity": "sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz", + "integrity": "sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/destination": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-6.5.0.tgz", + "integrity": "sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz", + "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "license": "MIT", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/kinks": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/kinks/-/kinks-6.5.0.tgz", + "integrity": "sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-intersect": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-6.5.0.tgz", + "integrity": "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-6.5.0.tgz", + "integrity": "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-split": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-6.5.0.tgz", + "integrity": "sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/nearest-point-on-line": "^6.5.0", + "@turf/square": "^6.5.0", + "@turf/truncate": "^6.5.0", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", + "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz", + "integrity": "sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/square/-/square-6.5.0.tgz", + "integrity": "sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/truncate": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-6.5.0.tgz", + "integrity": "sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.19", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.19.tgz", + "integrity": "sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/geojson-rbush": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", + "integrity": "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "*", + "@turf/helpers": "6.x", + "@turf/meta": "6.x", + "@types/geojson": "7946.0.8", + "rbush": "^3.0.1" + } + }, + "node_modules/geojson-rbush/node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", + "license": "MIT" + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet-extra-markers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/leaflet-extra-markers/-/leaflet-extra-markers-1.2.2.tgz", + "integrity": "sha512-LQUicXhyOwq0wOFtEftJg9IzAGcQI+6mHgh3wsVntrid149BTqsEgh1UcxPPtzcL7xt8jXY0rssfz7DUZuQudg==", + "peerDependencies": { + "leaflet": ">= 0.5 < 2" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/polyclip-ts": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/polyclip-ts/-/polyclip-ts-0.16.8.tgz", + "integrity": "sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.1.0", + "splaytree-ts": "^1.0.2" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proj4": { + "version": "2.19.4", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.19.4.tgz", + "integrity": "sha512-1l6JiJ2ZOzXIoo6k64diOQVOvHIF0IACMrHTaFHrEQmuo1tY1vb73mrWfTSyPH+muc0Lut4zuj5encvB1Ccuhg==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/ahocevar" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, + "node_modules/rollup": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", + "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.1", + "@rollup/rollup-android-arm64": "4.44.1", + "@rollup/rollup-darwin-arm64": "4.44.1", + "@rollup/rollup-darwin-x64": "4.44.1", + "@rollup/rollup-freebsd-arm64": "4.44.1", + "@rollup/rollup-freebsd-x64": "4.44.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", + "@rollup/rollup-linux-arm-musleabihf": "4.44.1", + "@rollup/rollup-linux-arm64-gnu": "4.44.1", + "@rollup/rollup-linux-arm64-musl": "4.44.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-musl": "4.44.1", + "@rollup/rollup-linux-s390x-gnu": "4.44.1", + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1", + "@rollup/rollup-win32-arm64-msvc": "4.44.1", + "@rollup/rollup-win32-ia32-msvc": "4.44.1", + "@rollup/rollup-win32-x64-msvc": "4.44.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/splaytree-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", + "integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==", + "license": "BDS-3-Clause" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", + "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/wkt-parser": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.2.tgz", + "integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..932a63e --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..3136762 --- /dev/null +++ b/src/interfaces.ts @@ -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 +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9165a4f --- /dev/null +++ b/src/main.ts @@ -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]); +}); \ No newline at end of file diff --git a/src/modules/maphandler/icons.ts b/src/modules/maphandler/icons.ts new file mode 100644 index 0000000..0b9341a --- /dev/null +++ b/src/modules/maphandler/icons.ts @@ -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] +}); \ No newline at end of file diff --git a/src/modules/maphandler/maphandler.css b/src/modules/maphandler/maphandler.css new file mode 100644 index 0000000..4b53a4e --- /dev/null +++ b/src/modules/maphandler/maphandler.css @@ -0,0 +1,11 @@ +.search-icon { + background-image: url('data:image/svg+xml,'); +} + +.exclude-icon { + background-image: url('data:image/svg+xml,'); +} + +.cancel-icon { + background-image: url('data:image/svg+xml,') +} \ No newline at end of file diff --git a/src/modules/maphandler/maphandler.ts b/src/modules/maphandler/maphandler.ts new file mode 100644 index 0000000..40f2242 --- /dev/null +++ b/src/modules/maphandler/maphandler.ts @@ -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: '© OpenStreetMap 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; \ No newline at end of file diff --git a/src/modules/worker/worker.ts b/src/modules/worker/worker.ts new file mode 100644 index 0000000..069142d --- /dev/null +++ b/src/modules/worker/worker.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..04e7223 --- /dev/null +++ b/src/style.css @@ -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; +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..151aa68 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/static/bicycle-solid.svg b/static/bicycle-solid.svg new file mode 100644 index 0000000..c31ed29 --- /dev/null +++ b/static/bicycle-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/marker-icon-2x-black.png b/static/marker-icon-2x-black.png new file mode 100644 index 0000000..23c94cf Binary files /dev/null and b/static/marker-icon-2x-black.png differ diff --git a/static/marker-icon-2x-blue.png b/static/marker-icon-2x-blue.png new file mode 100644 index 0000000..0015b64 Binary files /dev/null and b/static/marker-icon-2x-blue.png differ diff --git a/static/marker-icon-2x-gold.png b/static/marker-icon-2x-gold.png new file mode 100644 index 0000000..6992d65 Binary files /dev/null and b/static/marker-icon-2x-gold.png differ diff --git a/static/marker-icon-2x-green.png b/static/marker-icon-2x-green.png new file mode 100644 index 0000000..c359abb Binary files /dev/null and b/static/marker-icon-2x-green.png differ diff --git a/static/marker-icon-2x-grey.png b/static/marker-icon-2x-grey.png new file mode 100644 index 0000000..43b3eb4 Binary files /dev/null and b/static/marker-icon-2x-grey.png differ diff --git a/static/marker-icon-2x-orange.png b/static/marker-icon-2x-orange.png new file mode 100644 index 0000000..c3c8632 Binary files /dev/null and b/static/marker-icon-2x-orange.png differ diff --git a/static/marker-icon-2x-red.png b/static/marker-icon-2x-red.png new file mode 100644 index 0000000..1c26e9f Binary files /dev/null and b/static/marker-icon-2x-red.png differ diff --git a/static/marker-icon-2x-violet.png b/static/marker-icon-2x-violet.png new file mode 100644 index 0000000..ea748aa Binary files /dev/null and b/static/marker-icon-2x-violet.png differ diff --git a/static/marker-icon-2x-yellow.png b/static/marker-icon-2x-yellow.png new file mode 100644 index 0000000..8b677d9 Binary files /dev/null and b/static/marker-icon-2x-yellow.png differ diff --git a/static/marker-shadow.png b/static/marker-shadow.png new file mode 100644 index 0000000..84c5808 Binary files /dev/null and b/static/marker-shadow.png differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4f5edc2 --- /dev/null +++ b/tsconfig.json @@ -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"] +}