Compare commits

..
13 Commits
12 changed files with 1181 additions and 396 deletions
+8
View File
@@ -1,4 +1,12 @@
.gitattributes
.vscode/
dist/ dist/
native/* native/*
node_modules/ node_modules/
!native/route_search.cpp !native/route_search.cpp
*.sh
data_generation/database/
data_generation/data/
**/.ipynb_checkpoints/
**/.jupyter_ystore.db
**/*.dat
+12
View File
@@ -0,0 +1,12 @@
services:
database:
image: postgis/postgis:17-3.5
volumes:
- ./database/:/var/lib/postgresql/data/
environment:
POSTGRES_PASSWORD: database
POSTGRES_USER: database
POSTGRES_DB: database
PGDATA: /var/lib/postgresql/data
ports:
- 5432:5432
+529
View File
@@ -0,0 +1,529 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e74d75eb-5f32-4587-a6ee-a928fe6571e9",
"metadata": {},
"source": [
"# Setting up the database\n",
"Source data takes a lot of space, and might overwhelm the amount of RAM available on the computer. For this reason, we will use a database to work with the source data, so we can offload a bunch of the data from RAM to disk. The included compose.yaml file contains docker instructions to set up a PostgreSQL database with PostGIS. Run it first using\n",
"```bash\n",
"sudo docker compose up -d\n",
"```\n",
"\n",
"The next cell creates the tables used in the database. This requires the psycopg2 library."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "269a55a2-6c49-4871-95dd-2317d8066a83",
"metadata": {},
"outputs": [],
"source": [
"import psycopg2\n",
"\n",
"def initialize_database():\n",
"\tconnection = psycopg2.connect(database='database', host='localhost', user='database', password='database', port=5432)\n",
"\t\n",
"\twith connection:\n",
"\t\twith connection.cursor() as cursor:\n",
"\t\t\tcursor.execute('''\n",
"\t\t\t\tCREATE TABLE IF NOT EXISTS nodes(\n",
"\t\t\t\t\tid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\n",
"\t\t\t\t\tposition GEOMETRY(PointZ) NOT NULL\n",
"\t\t\t\t);\n",
"\t\t\t''')\n",
"\t\n",
"\t\t\tcursor.execute('''\n",
"\t\t\t\tCREATE TABLE IF NOT EXISTS edges(\n",
"\t\t\t\t\tid INTEGER GENERATED ALWAYS AS IDENTITY,\n",
"\t\t\t\t\tstart_point_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n",
"\t\t\t\t\tend_point_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n",
"\t\t\t\t\tmotorway BOOLEAN NOT NULL,\n",
"\t\t\t\t\tspeed_limit INTEGER NOT NULL,\n",
"\t\t\t\t\ttunnel BOOLEAN NOT NULL,\n",
"\t\t\t\t\tpassable_same_direction BOOLEAN NOT NULL,\n",
"\t\t\t\t\tpassable_opposite_direction BOOLEAN NOT NULL,\n",
"\t\t\t\t\tCONSTRAINT no_loops CHECK (start_point_id IS DISTINCT FROM end_point_id)\n",
"\t\t\t\t);\n",
"\t\t\t\tCREATE UNIQUE INDEX IF NOT EXISTS no_repeats ON edges (LEAST(start_point_id, end_point_id), GREATEST(start_point_id, end_point_id));\n",
"\t\t\t''')"
]
},
{
"cell_type": "markdown",
"id": "3fd2a33f-cce8-485b-837b-e0d5a1edaa2c",
"metadata": {},
"source": [
"# Constants"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f846657a-a1e9-4c0a-8c82-22ca75ffd416",
"metadata": {},
"outputs": [],
"source": [
"# Combine nodes closer than this distance from each other into the same node (in metres)\n",
"MINIMUM_NODE_DISTANCE = 1\n",
"# If we don't have a speed limit value in our data, use this as default\n",
"DEFAULT_SPEED_LIMIT = 0\n",
"# Support reading other data than just UTM33 for Norway\n",
"SOURCE_DATA_EPSG_CODE = \"EPSG:32633\""
]
},
{
"cell_type": "markdown",
"id": "35f4509f-3d5a-43de-9120-132f3fc5c8e4",
"metadata": {},
"source": [
"# Reading source data\n",
"Next, we need to read source data. The Norwegian Elveg 2.0 data is available in both SOSI and GML formats. I downloaded it in GML, since SOSI kind of seems old-fashioned. If you want to read other data types, write another conversion function below. Note that it is assumed that the units of the X, Y and Z coordinates are meters, so if necessary, convert the data into something like UML."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b9e34cb2-fdea-43f1-a09d-21f12b55ca3f",
"metadata": {},
"outputs": [],
"source": [
"from dataclasses import dataclass\n",
"from typing import Optional\n",
"import xml.etree.ElementTree as ET\n",
"import re\n",
"\n",
"@dataclass\n",
"class Node:\n",
"\tnode_id: str\n",
"\t\n",
"\tposition_x: float\n",
"\tposition_y: float\n",
"\tposition_z: Optional[float]\n",
"\n",
"@dataclass\n",
"class Edge:\n",
"\tstart_point_id: str\n",
"\tend_point_id: str\n",
"\n",
"\tspeed_limit: int\n",
"\tmotorway: bool\n",
"\ttunnel: bool\n",
"\tpassable_same_direction: bool\n",
"\tpassable_opposite_direction: bool\n",
"\n",
"@dataclass\n",
"class LoadedFile:\n",
"\tnodes: list[Node]\n",
"\tedges: list[Edge]\n",
"\n",
"\tx_extent: tuple[float, float]\n",
"\ty_extent: tuple[float, float]\n",
"\n",
"def read_gml_file(file_location: str, print_progress: bool = False) -> LoadedFile:\n",
"\tnodes: list[Node] = []\n",
"\tedges: list[Edge] = []\n",
"\n",
"\tx_extent = (1e99, -1e99)\n",
"\ty_extent = (1e99, -1e99)\n",
"\t\n",
"\tgeons = 'http://skjema.geonorge.no/SOSI/produktspesifikasjon/Elveg/2.0'\n",
"\tgmlns = 'http://www.opengis.net/gml/3.2'\n",
"\t\n",
"\ttree = ET.parse(file_location)\n",
"\troot = tree.getroot()\n",
"\n",
"\tmotorways: set[str] = set()\n",
"\tspeed_limits: dict[str, int] = {}\n",
"\n",
"\tfor motorway in root.findall(f'.//{{{geons}}}Motorveg'):\n",
"\t\tlocal_id = motorway.find(f'{{{geons}}}lineærPosisjon//{{{geons}}}lokalId').text\n",
"\t\tmotorways.add(local_id)\n",
"\n",
"\tfor speed_limit in root.findall(f'.//{{{geons}}}Fartsgrense'):\n",
"\t\tspeed_limit_value = int(speed_limit.find(f'.//{{{geons}}}fartsgrenseVerdi').text)\n",
"\t\tlocal_id = speed_limit.find(f'{{{geons}}}lineærPosisjon//{{{geons}}}lokalId').text\n",
"\t\tspeed_limits[local_id] = speed_limit_value\n",
"\n",
"\troad_links = root.findall(f'.//{{{geons}}}Veglenke')\n",
"\ttotal_road_links = len(road_links)\n",
"\tfor road_link_no, road_link in enumerate(road_links):\n",
"\t\tif print_progress and (road_link_no % 10000 == 0 or road_link_no+1 == total_road_links):\n",
"\t\t\tprint(f'\\rReading road link {road_link_no+1} of {total_road_links}'.ljust(50, ' '), end='')\n",
"\t\t\n",
"\t\troad_type = road_link.find(f'.//{{{geons}}}typeVeg').text\n",
"\t\t# If the road type is stairs, car ferry or passenger ferry, skip it\n",
"\t\tif road_type == 'trapp' or road_type == 'bilferje' or road_type == 'passasjerferje':\n",
"\t\t\tcontinue\n",
"\n",
"\t\tlocal_id = road_link.find(f'.//{{{geons}}}lokalId').text\n",
"\t\tmotorway = local_id in motorways\n",
"\t\tspeed_limit = DEFAULT_SPEED_LIMIT if not local_id in speed_limits else speed_limits[local_id]\n",
"\n",
"\t\t# Check if our road is under the terrain, under the sea bottom, or under a glacier (i.e. a tunnel)\n",
"\t\troad_link_medium = road_link.find(f'{{{geons}}}medium')\n",
"\t\tif road_link_medium is not None:\n",
"\t\t\troad_link_medium = road_link_medium.text\n",
"\t\ttunnel = road_link_medium == 'underTerrenget' or road_link_medium == 'underSjøbunnen' or road_link_medium == 'underIsbre'\n",
"\n",
"\t\t# However, if the road is a walking or biking path, we won't categorise it as a tunnel (since what we really want to\n",
"\t\t# avoid is road tunnels, and walking or biking path tunnels might just be underpasses or similar)\n",
"\t\tif road_type == 'gangOgSykkelveg' or road_type == 'sykkelveg' or road_type == 'gangveg':\n",
"\t\t\ttunnel = False\n",
"\n",
"\t\tpositions = road_link.find(f'.//{{{gmlns}}}posList')\n",
"\t\tcoordinates = [float(x) for x in positions.text.split(' ')]\n",
"\n",
"\t\t# The lanes describe whether this is a one-way road or not\n",
"\t\tlanes = road_link.find(f'.//{{{geons}}}feltoversikt')\n",
"\t\tif lanes is None:\n",
"\t\t\tpassable_same_direction = True\n",
"\t\t\tpassable_opposite_direction = False\n",
"\t\telse:\n",
"\t\t\tlanes = lanes.text.split('#')\n",
"\t\t\tpassable_same_direction = False\n",
"\t\t\tpassable_opposite_direction = False\n",
"\n",
"\t\t\tfor lane in lanes:\n",
"\t\t\t\tlane_number = int(re.search('^([0-9]+).*$', lane).group(1))\n",
"\t\t\t\tif lane_number % 2 == 1:\n",
"\t\t\t\t\tpassable_same_direction = True\n",
"\t\t\t\telse:\n",
"\t\t\t\t\tpassable_opposite_direction = True\n",
"\n",
"\t\t\treference_direction = road_link.find(f'.//{{{geons}}}referanseretning')\n",
"\t\t\tif reference_direction is not None and reference_direction.text == 'mot':\n",
"\t\t\t\tpassable_same_direction, passable_opposite_direction = passable_opposite_direction, passable_same_direction\n",
"\n",
"\t\tx_coordinates = coordinates[0::3]\n",
"\t\ty_coordinates = coordinates[1::3]\n",
"\t\tz_coordinates = coordinates[2::3]\n",
"\n",
"\t\tcurrent_node = None\n",
"\n",
"\t\tfor i in range(len(x_coordinates)):\n",
"\t\t\tprevious_node = current_node\n",
"\n",
"\t\t\tcurrent_x = x_coordinates[i]\n",
"\t\t\tcurrent_y = y_coordinates[i]\n",
"\t\t\tcurrent_z = z_coordinates[i]\n",
"\n",
"\t\t\tx_extent = (min(current_x, x_extent[0]), max(current_x, x_extent[1]))\n",
"\t\t\ty_extent = (min(current_y, y_extent[0]), max(current_y, y_extent[1]))\n",
"\n",
"\t\t\tnode_id = str(road_link_no).rjust(15, ' ') + str(i).rjust(15, ' ')\n",
"\t\t\tcurrent_node = Node(node_id, current_x, current_y, current_z)\n",
"\t\t\tnodes.append(current_node)\n",
"\n",
"\t\t\tif previous_node is not None:\n",
"\t\t\t\tedge = Edge(previous_node.node_id, current_node.node_id, speed_limit, motorway, tunnel, passable_same_direction, passable_opposite_direction)\n",
"\t\t\t\tedges.append(edge)\n",
"\n",
"\treturn LoadedFile(nodes, edges, x_extent, y_extent)"
]
},
{
"cell_type": "markdown",
"id": "963065c7-91ad-431c-9981-2c7efa65136f",
"metadata": {},
"source": [
"# Add loaded file to database\n",
"Since the Norwegian GML data is something like 14 GB all together, we won't try to load it all into memory. Instead, we will write the files, one by one, into a database, and later we will read from the database to create a file. At least in the GML data, some nodes don't have good altitudes, so it is possible to provide a function for correcting strange altitudes. This will fire if an altitude is either None or less than -3000."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "43501a49-f2b6-441e-9a8c-8f6e16c77431",
"metadata": {},
"outputs": [],
"source": [
"import psycopg2\n",
"import math\n",
"from collections import defaultdict\n",
"from typing import Optional, Callable\n",
"\n",
"def add_to_database(loadedFile: LoadedFile, correct_altitude_function: Optional[Callable[[float, float], float]] = None, print_progress: bool = False):\n",
"\tconnection = psycopg2.connect(database='database', host='localhost', user='database', password='database', port=5432)\n",
"\twith connection:\n",
"\t\twith connection.cursor() as cursor:\n",
"\t\t\t# Store previously defined nodes in a dict, using their geographical position as key\n",
"\t\t\t# The keys are a tuple containing the point's X and Y coordinates, rounded to the closest MINIMUM_NODE_DISTANCE\n",
"\t\t\t# The values are a lists containing up to several points, where each point is represented as a tuple containing\n",
"\t\t\t# the point's database ID, and its actual X, Y and Z coordinates\n",
"\t\t\tdefined_points: dict[tuple[int, int], list[tuple[int, float, float, float]]] = defaultdict(list)\n",
"\t\t\n",
"\t\t\t# Load all points that are already in the database that overlap with the extents of the current file, so we can combine previously added nodes\n",
"\t\t\tmin_x = loadedFile.x_extent[0] - 2 * MINIMUM_NODE_DISTANCE\n",
"\t\t\tmax_x = loadedFile.x_extent[1] + 2 * MINIMUM_NODE_DISTANCE\n",
"\t\t\tmin_y = loadedFile.y_extent[0] - 2 * MINIMUM_NODE_DISTANCE\n",
"\t\t\tmax_y = loadedFile.y_extent[1] + 2 * MINIMUM_NODE_DISTANCE\n",
"\t\t\tpolygon_wkt = f'POLYGON(({min_x} {min_y}, {min_x} {max_y}, {max_x} {max_y}, {max_x} {min_y}, {min_x} {min_y}))'\n",
"\t\t\tcursor.execute(f'SELECT id, ST_X(position), ST_Y(position), ST_Z(position) FROM nodes WHERE ST_Within(position, ST_GeomFromText(\\'{polygon_wkt}\\'))')\n",
"\t\t\tfor node_id, x, y, z in cursor:\n",
"\t\t\t\trounded_x = int(round(x / MINIMUM_NODE_DISTANCE))\n",
"\t\t\t\trounded_y = int(round(y / MINIMUM_NODE_DISTANCE))\n",
"\t\t\t\tdefined_points[(rounded_x, rounded_y)].append((node_id, x, y, z))\n",
"\t\t\n",
"\t\t\tnode_id_to_database_id: dict[str, int] = {}\n",
"\t\t\n",
"\t\t\t# Add all nodes to database\n",
"\t\t\tnumber_of_nodes = len(loadedFile.nodes)\n",
"\t\t\tfor node_number, node in enumerate(loadedFile.nodes):\n",
"\t\t\t\tif print_progress and (node_number % 10000 == 0 or node_number+1 == number_of_nodes):\n",
"\t\t\t\t\tprint(f'\\rStoring node {node_number+1} of {number_of_nodes}'.ljust(50, ' '), end='')\n",
"\t\t\t\t# Correct node altitude if necessary and possible\n",
"\t\t\t\tif correct_altitude_function is not None and (node.position_z is None or node.position_z < -3000):\n",
"\t\t\t\t\tnode.position_z = correct_altitude_function(node.position_x, node.position_y)\n",
"\n",
"\t\t\t\t# If all else fails, just say the node is at sea level\n",
"\t\t\t\tif node.position_z is None:\n",
"\t\t\t\t\tnode.position_z = 0\n",
"\t\t\t\t\n",
"\t\t\t\trounded_x = int(round(node.position_x / MINIMUM_NODE_DISTANCE))\n",
"\t\t\t\trounded_y = int(round(node.position_y / MINIMUM_NODE_DISTANCE))\n",
"\t\t\n",
"\t\t\t\t# Check if a node within minimum distance already exists\n",
"\t\t\t\tclosest_node_id = None\n",
"\t\t\t\tclosest_node_distance = 1e99\n",
"\t\t\t\tfor dx in [-1, 0, 1]:\n",
"\t\t\t\t\tfor dy in [-1, 0, 1]:\n",
"\t\t\t\t\t\tfor other_node_id, other_node_x, other_node_y, other_node_z in defined_points[(rounded_x + dx, rounded_y + dy)]:\n",
"\t\t\t\t\t\t\tdistance = math.sqrt((other_node_x - node.position_x)**2 + (other_node_y - node.position_y)**2 + (other_node_z - node.position_z)**2)\n",
"\t\t\t\t\t\t\tif distance < closest_node_distance:\n",
"\t\t\t\t\t\t\t\tclosest_node_distance = distance\n",
"\t\t\t\t\t\t\t\tclosest_node_id = other_node_id\n",
"\t\t\n",
"\t\t\t\t# If a node within the closest distance already exists, store this\n",
"\t\t\t\tif closest_node_distance < MINIMUM_NODE_DISTANCE:\n",
"\t\t\t\t\tnode_id_to_database_id[node.node_id] = closest_node_id\n",
"\t\t\t\telse:\n",
"\t\t\t\t\tcursor.execute(f'INSERT INTO nodes(position) VALUES(ST_GeomFromText(\\'POINT({node.position_x} {node.position_y} {node.position_z})\\')) RETURNING ID;')\n",
"\t\t\t\t\tnode_db_id = cursor.fetchone()[0]\n",
"\t\t\t\t\tnode_id_to_database_id[node.node_id] = node_db_id\n",
"\t\t\t\t\tdefined_points[(rounded_x, rounded_y)].append((node_db_id, node.position_x, node.position_y, node.position_z))\n",
"\t\t\t\t\t\n",
"\t\t\t# Next, add all the edges\n",
"\t\t\t# Store which edges we have already added, to avoid violating constraint\n",
"\t\t\texisting_edges = set()\n",
"\t\t\tnumber_of_edges = len(loadedFile.edges)\n",
"\t\t\tfor edge_number, edge in enumerate(loadedFile.edges):\n",
"\t\t\t\tif print_progress and (edge_number % 10000 == 0 or edge_number+1 == number_of_edges):\n",
"\t\t\t\t\tprint(f'\\rStoring edge {edge_number+1} of {number_of_edges}'.ljust(50, ' '), end='')\n",
"\t\t\t\tstart_node_id = node_id_to_database_id[edge.start_point_id]\n",
"\t\t\t\tend_node_id = node_id_to_database_id[edge.end_point_id]\n",
"\t\t\t\t\n",
"\t\t\t\tif start_node_id == end_node_id:\n",
"\t\t\t\t\tcontinue\n",
"\n",
"\t\t\t\tadded_edge = (min(start_node_id, end_node_id), max(start_node_id, end_node_id))\n",
"\t\t\t\tif added_edge in existing_edges:\n",
"\t\t\t\t\tcontinue\n",
"\t\t\t\texisting_edges.add(added_edge)\n",
"\n",
"\t\t\t\tcursor.execute(f'''\n",
"\t\t\t\t\tINSERT INTO edges(\n",
"\t\t\t\t\t\tstart_point_id,\n",
"\t\t\t\t\t\tend_point_id,\n",
"\t\t\t\t\t\tmotorway,\n",
"\t\t\t\t\t\tspeed_limit,\n",
"\t\t\t\t\t\ttunnel,\n",
"\t\t\t\t\t\tpassable_same_direction,\n",
"\t\t\t\t\t\tpassable_opposite_direction\n",
"\t\t\t\t\t\t)\n",
"\t\t\t\t\tVALUES(\n",
"\t\t\t\t\t\t{start_node_id},\n",
"\t\t\t\t\t\t{end_node_id},\n",
"\t\t\t\t\t\t{edge.motorway},\n",
"\t\t\t\t\t\t{edge.speed_limit},\n",
"\t\t\t\t\t\t{edge.tunnel},\n",
"\t\t\t\t\t\t{edge.passable_same_direction},\n",
"\t\t\t\t\t\t{edge.passable_opposite_direction}\n",
"\t\t\t\t\t);\n",
"\t\t\t\t''')"
]
},
{
"cell_type": "markdown",
"id": "19525a68-a5a6-44a9-b427-f1fd98872556",
"metadata": {},
"source": [
"# Write data file from database\n",
"Try to pack the file as tightly as possible. Since we represent the road network as a mathematical graph, we need to store the edges and the nodes of the graph. We first store the nodes, and then the edges.\n",
"\n",
"First, a 4-byte integer says how many nodes are stored in the file. Then, each node consists of a 4-byte float for its x position, a 4-byte float for its y position, and a 2-byte integer for its height. Heights are represented as integers by adding 3000, rounding to the closest 10 centimetres, and then multiplying by 10. In this way, heights between around -3000 and +3500 metres can be stored. This needs some modification if you want to do this in a country with higher mountains than Norway. No ID is stored for the nodes; its position in the list is used as ID.\n",
"\n",
"Edges are similarly stored, using first a 4-byte integer that says how many edges are contained. Each edge then contains a 4-byte integer containing the ID of the node it starts from, and a 4-byte integer containing the ID of the node it leads to. Finally, a single byte contains the rest of the information about the edge. The first 4 bits of this byte contains the edge's speed limit, divided by 10. The next bit is a flag saying whether the node is passable when going against its defined direction (i.e. from the end node to the start node), and the bit after that says whether the edge is passable when going in the same direction as the edge. This is meant to represent whether the edge is a one-way street. The next bit is a flag for whether the edge is a tunnel, and then final bit is a flag saying whether the edge is a motorway.\n",
"\n",
"Even though it would make more sense to write the EPSG code of the coordinates at the beginning of the file, we'll write them at the end of the file for backwards compatibility. If this string is not present, the web site will assume the data is in EPSG:32633."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e740021a-0cd2-49bb-9751-34f76b03fa3c",
"metadata": {},
"outputs": [],
"source": [
"import struct\n",
"import psycopg2\n",
"\n",
"def write_data_file(file_name: str, print_progress: bool = False):\n",
"\twith open(file_name, 'wb') as roads_file:\n",
"\t\tconnection = psycopg2.connect(database='database', host='localhost', user='database', password='database', port=5432)\n",
"\t\twith connection:\n",
"\t\t\twith connection.cursor() as cursor:\n",
"\t\t\t\tcursor.execute('SELECT COUNT(*) FROM nodes;')\n",
"\t\t\t\t\n",
"\t\t\t\t(number_of_points,) = cursor.fetchone()\n",
"\t\t\t\troads_file.write(number_of_points.to_bytes(4, 'big'))\n",
"\n",
"\t\t\t\tcursor.execute('SELECT id, ST_X(position), ST_Y(position), ST_Z(position) FROM nodes ORDER BY id ASC')\n",
"\t\t\t\tid_to_position_map = {}\n",
"\t\t\t\tcounter = 0\n",
"\t\t\t\tfor node_id, x, y, z in cursor:\n",
"\t\t\t\t\tid_to_position_map[node_id] = counter\n",
"\t\t\t\t\tcounter += 1\n",
"\n",
"\t\t\t\t\tif print_progress and (counter % 10000 == 0 or counter == number_of_points):\n",
"\t\t\t\t\t\tprint(f'\\rWriting node {counter} of {number_of_points} to file'.ljust(50, ' '), end='')\n",
"\n",
"\t\t\t\t\troads_file.write(struct.pack('>f', x))\n",
"\t\t\t\t\troads_file.write(struct.pack('>f', y))\n",
"\n",
"\t\t\t\t\tif z > 3500 or z < -3000:\n",
"\t\t\t\t\t\tz = 0\n",
"\n",
"\t\t\t\t\theight_int = int(round(z + 3000) * 10)\n",
"\t\t\t\t\troads_file.write(height_int.to_bytes(2, 'big'))\n",
"\n",
"\t\t\t\tcursor.execute('SELECT COUNT(*) FROM edges;')\n",
"\t\t\t\t(number_of_edges,) = cursor.fetchone()\n",
"\n",
"\t\t\t\troads_file.write(number_of_edges.to_bytes(4, 'big'))\n",
"\t\t\t\tcursor.execute('''\n",
"\t\t\t\t\tSELECT\n",
"\t\t\t\t\t\tstart_point_id,\n",
"\t\t\t\t\t\tend_point_id,\n",
"\t\t\t\t\t\tmotorway,\n",
"\t\t\t\t\t\tspeed_limit,\n",
"\t\t\t\t\t\ttunnel,\n",
"\t\t\t\t\t\tpassable_same_direction,\n",
"\t\t\t\t\t\tpassable_opposite_direction\n",
"\t\t\t\t\tFROM edges ORDER BY id ASC;'''\n",
"\t\t\t\t)\n",
"\n",
"\t\t\t\tcounter = 0\n",
"\t\t\t\tfor start_point_id, end_point_id, motorway, speed_limit, tunnel, passable_same_direction, passable_opposite_direction in cursor:\n",
"\t\t\t\t\troads_file.write(id_to_position_map[start_point_id].to_bytes(4, 'big'))\n",
"\t\t\t\t\troads_file.write(id_to_position_map[end_point_id].to_bytes(4, 'big'))\n",
"\n",
"\t\t\t\t\tcounter += 1\n",
"\t\t\t\t\tif print_progress and (counter % 10000 == 0 or counter == number_of_edges):\n",
"\t\t\t\t\t\tprint(f'\\rWriting edge {counter} of {number_of_edges} to file'.ljust(50, ' '), end='')\n",
"\n",
"\t\t\t\t\tflags_integer = int(speed_limit / 10) << 4\n",
"\t\t\t\t\tif motorway:\n",
"\t\t\t\t\t\tflags_integer += 1\n",
"\t\t\t\t\tif tunnel:\n",
"\t\t\t\t\t\tflags_integer += 2\n",
"\t\t\t\t\tif passable_same_direction:\n",
"\t\t\t\t\t\tflags_integer += 4\n",
"\t\t\t\t\tif passable_opposite_direction:\n",
"\t\t\t\t\t\tflags_integer += 8\n",
"\t\t\t\t\troads_file.write(flags_integer.to_bytes(1, 'big'))\n",
"\n",
"\t\t# Write EPSG code\n",
"\t\tepsg_code_bytes = SOURCE_DATA_EPSG_CODE.encode('utf-8')\n",
"\t\troads_file.write(len(epsg_code_bytes).to_bytes(4, 'big'))\n",
"\t\troads_file.write(epsg_code_bytes)"
]
},
{
"cell_type": "markdown",
"id": "6e718eae-a595-407f-8d0a-62d1b89206e7",
"metadata": {},
"source": [
"# Correct strange altitudes\n",
"For the Norwegian GML data, we will query an elevation API for strange heights, and just assume that the road at that point is at terrain level."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5b49d8e-1b17-4836-8ac7-2839a3ebb340",
"metadata": {},
"outputs": [],
"source": [
"import requests\n",
"import json\n",
"\n",
"def get_actual_elevation(x, y):\n",
"\tparameters = {\n",
"\t\t'koordsys': '32633',\n",
"\t\t'punkter': f'[[{x},{y}]]',\n",
"\t\t'geojson': False\n",
"\t}\n",
"\treturn json.loads(requests.get('https://ws.geonorge.no/hoydedata/v1/punkt', params=parameters).text)[\"punkter\"][0][\"z\"]"
]
},
{
"cell_type": "markdown",
"id": "041cc46e-373b-44d2-afaa-fb4fe20575e4",
"metadata": {},
"source": [
"# Build data file from GML\n",
"In this case, all the GML files are stored in the folder named data. Read all files in this folder, store them to database, and finally write the database to file."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c51308af-9ca1-4dc4-aed4-dc97100864ef",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"initialize_database()\n",
"\n",
"gml_files = os.listdir('./data')\n",
"for file_number, gml_file in enumerate(gml_files):\n",
"\tprint(f'\\rReading {gml_file}, number {file_number+1} of {len(gml_files)}'.ljust(50, ' '))\n",
"\tdata_file = read_gml_file(f'./data/{gml_file}', print_progress=True)\n",
"\tadd_to_database(data_file, get_actual_elevation, print_progress=True)\n",
"write_data_file('roads.dat', print_progress=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1811fcd5-d2c8-4433-ad44-925dbbeca359",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+14 -2
View File
@@ -31,12 +31,15 @@
<div id="map"></div> <div id="map"></div>
<br /> <br />
<button id="search-button">Start search</button> <button id="search-button">Start search</button>
<button id="export-route-button">Export GPX route</button> <button id="export-route-button">Export KML route</button>
<br />
<label for="reverse-search-input">Reverse search:</label><input type="checkbox" id="reverse-search-input" />
<br />
<p id="search-status-paragraph"></p> <p id="search-status-paragraph"></p>
<table id="search-result-table" style="display: none;"> <table id="search-result-table" style="display: none;">
<thead> <thead>
<tr> <tr>
<th>Distance</th><th>Latitude</th><th>Longitude</th><th>Show in map</th> <th>Distance</th><th class="hidable">Latitude</th><th class="hidable">Longitude</th><th>Show in map</th>
</tr> </tr>
</thead> </thead>
<tbody id="search-result-table-body"> <tbody id="search-result-table-body">
@@ -63,6 +66,9 @@
<div class="settings-line"> <div class="settings-line">
<label for="allow-wrong-way-input">Allow going the wrong way down one-way roads:</label><input id="allow-wrong-way-input" type="checkbox" /> <label for="allow-wrong-way-input">Allow going the wrong way down one-way roads:</label><input id="allow-wrong-way-input" type="checkbox" />
</div> </div>
<div class="settings-line">
<label for="limit-corner-speed-input">Limit speed on sharp corners</label><input id="limit-corner-speed-input" type="checkbox" />
</div>
<div class="settings-line"> <div class="settings-line">
<label for="drag-coefficient-input">Drag coeffient (0.005 is a good default):</label><input id="drag-coefficient-input" type="number" min="0" step="0.001" /> <label for="drag-coefficient-input">Drag coeffient (0.005 is a good default):</label><input id="drag-coefficient-input" type="number" min="0" step="0.001" />
</div> </div>
@@ -77,6 +83,12 @@
end points are shown as purple circles. Click an endpoint to see the calculated route that leads from the start point end points are shown as purple circles. Click an endpoint to see the calculated route that leads from the start point
to the end point. to the end point.
</p> </p>
<p>
It is also possible to reverse the search, to find the farthest start point from a given end point. When this is done,
farthest possible start point is shown in green, and other candidate start points are shown in orange. Due to the way the
reverse search works, many starting points will require you to start near the top speed (as opposed to normal search, where
you are always assumed to start at the minimum speed).
</p>
<p> <p>
It is possible to mark areas on the map you would like to avoid. Click the "Draw areas that should not be It is possible to mark areas on the map you would like to avoid. Click the "Draw areas that should not be
entered" button at the top left of the map. It is marked with this icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="20px"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --> <path d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>. entered" button at the top left of the map. It is marked with this icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="20px"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --> <path d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>.
+391 -228
View File
@@ -1,11 +1,16 @@
#ifdef __EMSCRIPTEN__
#include <emscripten/bind.h> #include <emscripten/bind.h>
#include <emscripten/val.h> #include <emscripten/val.h>
#endif
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <vector>
#include <map> #include <map>
#include <set> #include <set>
#include <cmath>
#include <algorithm>
// Constants // Constants
const float GRAVITY_ACCELERATION = 9.81; const float GRAVITY_ACCELERATION = 9.81;
@@ -13,22 +18,24 @@ const float GRAVITY_ACCELERATION = 9.81;
// Data structures // Data structures
struct Connection { struct Connection {
int connected_point_number; int connectedPointNumber;
float distance; float distance;
uint8_t speed_limit; float course;
uint8_t speedLimit;
bool motorway; bool motorway;
bool tunnel; bool tunnel;
bool againstOneWay; bool passableSameDirection;
bool passableOppositeDirection;
}; };
struct RoadNode{ struct RoadNode{
float position_x; float positionX;
float position_y; float positionY;
float position_z; float positionZ;
Connection connection_one; Connection connectionOne;
Connection connection_two; Connection connectionTwo;
std::vector<Connection> *extra_connections; std::vector<Connection> *extraConnections;
}; };
struct RoadNodeSet { struct RoadNodeSet {
@@ -45,6 +52,7 @@ struct SearchResult {
uint32_t startingNode; uint32_t startingNode;
std::map<uint32_t, uint32_t> previous; std::map<uint32_t, uint32_t> previous;
std::map<uint32_t, SearchNodeInfo> reachableNodes; std::map<uint32_t, SearchNodeInfo> reachableNodes;
bool reverse;
}; };
struct JSNodeInfo { struct JSNodeInfo {
@@ -59,11 +67,13 @@ struct JSNodeInfo {
struct JSSearchResult { struct JSSearchResult {
std::vector<JSNodeInfo> endPoints; std::vector<JSNodeInfo> endPoints;
bool reverse;
}; };
struct ListNode { struct ListNode {
int id; int id;
float current_speed; float currentSpeed;
float currentCourse;
ListNode* next = NULL; ListNode* next = NULL;
}; };
@@ -93,14 +103,17 @@ struct CurrentAreaSearch {
bool allowMotorways; bool allowMotorways;
bool allowTunnels; bool allowTunnels;
bool allowAgainstOneway; bool allowAgainstOneway;
bool limitCornerSpeed;
std::vector<uint32_t> startNodes; std::vector<uint32_t> startNodes;
size_t startNodeCounter = 0; size_t startNodeCounter = 0;
std::vector<AreaSearchEntry> currentAreaSearchResults; std::vector<AreaSearchEntry> currentAreaSearchResults;
std::set<uint32_t> utilizedNodes; std::map<uint32_t, std::vector<uint32_t>> utilizedNodes;
}; };
// Data // Data
// For backwards compatibility, default code is EPSG:32633 (UTM zone 33)
char const *epsgCode = "EPSG:32633";
RoadNodeSet set; RoadNodeSet set;
SearchResult lastSearchResult; SearchResult lastSearchResult;
std::set<uint32_t> excludedNodes; std::set<uint32_t> excludedNodes;
@@ -114,32 +127,35 @@ float getFloatFromBuffer(char* buffer) {
return *((float*) &correctByteOrder); return *((float*) &correctByteOrder);
} }
void addLinkToRoadNode(RoadNode* roadNodes, int node_from, int node_to, uint8_t speed_limit, bool motorway, bool tunnel, bool againstOneWay) { void addLinkToRoadNode(RoadNode* roadNodes, uint32_t nodeFrom, uint32_t nodeTo, uint8_t speedLimit, bool motorway, bool tunnel, bool passableSameDirection, bool passableOppositeDirection) {
float from_x = roadNodes[node_from].position_x; float fromX = roadNodes[nodeFrom].positionX;
float from_y = roadNodes[node_from].position_y; float fromY = roadNodes[nodeFrom].positionY;
float to_x = roadNodes[node_to].position_x; float toX = roadNodes[nodeTo].positionX;
float to_y = roadNodes[node_to].position_y; float toY = roadNodes[nodeTo].positionY;
float distance = sqrt(pow(to_x - from_x, 2) + pow(to_y - from_y, 2)); float distance = sqrt(pow(toX - fromX, 2) + pow(toY - fromY, 2));
float course = atan2(toX - fromX, toY - fromY) * 180 / 3.14152965;
Connection connection; Connection connection;
connection.connected_point_number = node_to; connection.connectedPointNumber = nodeTo;
connection.distance = distance; connection.distance = distance;
connection.speed_limit = speed_limit; connection.course = course;
connection.speedLimit = speedLimit;
connection.motorway = motorway; connection.motorway = motorway;
connection.tunnel = tunnel; connection.tunnel = tunnel;
connection.againstOneWay = againstOneWay; connection.passableSameDirection = passableSameDirection;
connection.passableOppositeDirection = passableOppositeDirection;
if (roadNodes[node_from].connection_one.connected_point_number == -1) { if (roadNodes[nodeFrom].connectionOne.connectedPointNumber == -1) {
roadNodes[node_from].connection_one = connection; roadNodes[nodeFrom].connectionOne = connection;
} else if (roadNodes[node_from].connection_two.connected_point_number == -1) { } else if (roadNodes[nodeFrom].connectionTwo.connectedPointNumber == -1) {
roadNodes[node_from].connection_two = connection; roadNodes[nodeFrom].connectionTwo = connection;
} else { } else {
if (roadNodes[node_from].extra_connections == NULL) { if (roadNodes[nodeFrom].extraConnections == NULL) {
roadNodes[node_from].extra_connections = new std::vector<Connection>(); roadNodes[nodeFrom].extraConnections = new std::vector<Connection>();
} }
roadNodes[node_from].extra_connections->push_back(connection); roadNodes[nodeFrom].extraConnections->push_back(connection);
} }
} }
@@ -149,73 +165,91 @@ void loadData(std::string filePath) {
std::ifstream source(filePath.c_str(), std::ios_base::binary); std::ifstream source(filePath.c_str(), std::ios_base::binary);
char *buffer = new char[4]; char *buffer = new char[4];
source.read(buffer, 4); source.read(buffer, 4);
uint32_t number_of_entries = ntohl(*reinterpret_cast<uint32_t*>(buffer)); uint32_t numberOfEntries = ntohl(*reinterpret_cast<uint32_t*>(buffer));
std::cout << "Reading " << number_of_entries << " road nodes" << std::endl; std::cout << "Reading " << numberOfEntries << " road nodes" << std::endl;
// Create the memory space for all these road nodes // Create the memory space for all these road nodes
set.numberOfNodes = number_of_entries; set.numberOfNodes = numberOfEntries;
set.roadNodes = new RoadNode[number_of_entries]; set.roadNodes = new RoadNode[numberOfEntries];
for (size_t i = 0; i < number_of_entries; i++) { for (size_t i = 0; i < numberOfEntries; i++) {
// Each node in the file is of the type float x, float y, short z // Each node in the file is of the type float x, float y, short z
source.read(buffer, 4); source.read(buffer, 4);
// First, cast this to an int, reverse the byte order, and then cast to float // First, cast this to an int, reverse the byte order, and then cast to float
float position_x = getFloatFromBuffer(buffer); float positionX = getFloatFromBuffer(buffer);
source.read(buffer, 4); source.read(buffer, 4);
float position_y = getFloatFromBuffer(buffer); float positionY = getFloatFromBuffer(buffer);
source.read(buffer, 2); source.read(buffer, 2);
int position_z_int = ntohs(*reinterpret_cast<uint16_t*>(buffer)); int positionZInt = ntohs(*reinterpret_cast<uint16_t*>(buffer));
float position_z = (position_z_int - 30000) / 10.0; float positionZ = (positionZInt - 30000) / 10.0;
set.roadNodes[i].position_x = position_x; set.roadNodes[i].positionX = positionX;
set.roadNodes[i].position_y = position_y; set.roadNodes[i].positionY = positionY;
set.roadNodes[i].position_z = position_z; set.roadNodes[i].positionZ = positionZ;
set.roadNodes[i].connection_one.connected_point_number = -1; set.roadNodes[i].connectionOne.connectedPointNumber = -1;
set.roadNodes[i].connection_two.connected_point_number = -1; set.roadNodes[i].connectionTwo.connectedPointNumber = -1;
set.roadNodes[i].extra_connections = NULL; set.roadNodes[i].extraConnections = NULL;
} }
source.read(buffer, 4); source.read(buffer, 4);
uint32_t number_of_links = ntohl(*reinterpret_cast<uint32_t*>(buffer)); uint32_t numberOfLinks = ntohl(*reinterpret_cast<uint32_t*>(buffer));
std::cout << "Reading " << number_of_links << " links" << std::endl; std::cout << "Reading " << numberOfLinks << " links" << std::endl;
// Read all the links between nodes // Read all the links between nodes
int connection_vectors = 0; for (size_t i = 0; i < numberOfLinks; i++) {
for (size_t i = 0; i < number_of_links; i++) {
source.read(buffer, 4); source.read(buffer, 4);
uint32_t from_point = ntohl(*reinterpret_cast<uint32_t*>(buffer)); uint32_t fromPoint = ntohl(*reinterpret_cast<uint32_t*>(buffer));
source.read(buffer, 4); source.read(buffer, 4);
uint32_t to_point = ntohl(*reinterpret_cast<uint32_t*>(buffer)); uint32_t toPoint = ntohl(*reinterpret_cast<uint32_t*>(buffer));
source.read(buffer, 1); source.read(buffer, 1);
uint8_t flags_byte = *reinterpret_cast<uint8_t*>(buffer); uint8_t flagsByte = *reinterpret_cast<uint8_t*>(buffer);
uint8_t speed_limit = (flags_byte >> 4) * 10; uint8_t speedLimit = (flagsByte >> 4) * 10;
bool motorway = (flags_byte & 0x01) > 0; bool motorway = (flagsByte & 0x01) > 0;
bool tunnel = (flags_byte & 0x02) > 0; bool tunnel = (flagsByte & 0x02) > 0;
bool passable_same_direction = (flags_byte & 0x04) > 0; bool passableSameDirection = (flagsByte & 0x04) > 0;
bool passable_opposite_direction = (flags_byte & 0x08) > 0; bool passableOppositeDirection = (flagsByte & 0x08) > 0;
addLinkToRoadNode(set.roadNodes, from_point, to_point, speed_limit, motorway, tunnel, !passable_same_direction); addLinkToRoadNode(set.roadNodes, fromPoint, toPoint, speedLimit, motorway, tunnel, passableSameDirection, passableOppositeDirection);
addLinkToRoadNode(set.roadNodes, to_point, from_point, speed_limit, motorway, tunnel, !passable_opposite_direction); addLinkToRoadNode(set.roadNodes, toPoint, fromPoint, speedLimit, motorway, tunnel, passableOppositeDirection, passableSameDirection);
}
// Finally, check if the file contains a string describing the EPSG code of its coordinates. For backwards compatibility, this
// field may not be there
source.read(buffer, 4);
if (!source.eof()) {
uint32_t lengthOfString = ntohl(*reinterpret_cast<uint32_t*>(buffer));
// I think it is technically a memory leak to create this buffer without eventually destroying it, but it should
// only happen once, and it's tiny, so I don't care.
char* stringBuffer = new char[lengthOfString+1];
source.read(stringBuffer, lengthOfString);
*(stringBuffer + lengthOfString) = 0;
epsgCode = stringBuffer;
} }
delete[] buffer; delete[] buffer;
} }
// Get the EPSG code
std::string getEpsgCode() {
return std::string(epsgCode);
}
// Search functions // Search functions
JSNodeInfo findClosestNode(float position_x, float position_y) { JSNodeInfo findClosestNode(float positionX, float positionY) {
float closestDistance = 1e99; float closestDistance = 1e99;
uint32_t closestNode = 0; uint32_t closestNode = 0;
for (size_t i = 0; i < set.numberOfNodes; i++) { for (size_t i = 0; i < set.numberOfNodes; i++) {
RoadNode node = set.roadNodes[i]; RoadNode node = set.roadNodes[i];
float node_x = node.position_x; float nodeX = node.positionX;
float node_y = node.position_y; float nodeY = node.positionY;
float distance = sqrt(pow(position_x - node_x, 2) + pow(position_y - node_y, 2)); float distance = sqrt(pow(positionX - nodeX, 2) + pow(positionY - nodeY, 2));
if (distance < closestDistance) { if (distance < closestDistance) {
closestDistance = distance; closestDistance = distance;
closestNode = i; closestNode = i;
@@ -224,112 +258,145 @@ JSNodeInfo findClosestNode(float position_x, float position_y) {
JSNodeInfo result; JSNodeInfo result;
result.nodeId = closestNode; result.nodeId = closestNode;
result.positionX = set.roadNodes[closestNode].position_x; result.positionX = set.roadNodes[closestNode].positionX;
result.positionY = set.roadNodes[closestNode].position_y; result.positionY = set.roadNodes[closestNode].positionY;
result.positionZ = set.roadNodes[closestNode].position_z; result.positionZ = set.roadNodes[closestNode].positionZ;
return result; return result;
} }
float calculate_speed(float starting_speed, float horizontal_distance, float height_difference, float minimum_speed, float maximum_speed, float drag_coefficient) { float calculateSpeed(float startingSpeed, float horizontalDistance, float heightDifference, float dragCoefficient) {
float slope_tan = height_difference / horizontal_distance; float slopeTan = heightDifference / horizontalDistance;
float final_speed = -1;
// If the slope is flat, that is one calculation // If the slope is flat, that is one calculation
if (fabs(slope_tan) < 0.0001) { if (fabs(slopeTan) < 0.0001) {
float time_to_finish = (exp(horizontal_distance * drag_coefficient) - 1) / (starting_speed * drag_coefficient); return startingSpeed * exp(-dragCoefficient * horizontalDistance);
final_speed = starting_speed / (starting_speed * drag_coefficient * time_to_finish + 1); }
} else {
// Otherwise, we need to find some parameters // If the slope is not flat, we should calculate some trig identities, and how long the slope is
float slope = atan(slope_tan); float slope = atan(slopeTan);
float slope_sin = sin(slope); float slopeSin = sin(slope);
float full_distance = horizontal_distance * slope_tan / slope_sin; float fullDistance = horizontalDistance * slopeTan / slopeSin;
float acceleration = -GRAVITY_ACCELERATION * slope_sin;
float terminal_velocity = sqrt(fabs(acceleration) / drag_coefficient); // We need to calculate the terminal velocity given the slope we're in
float terminalVelocity = sqrt(fabs(GRAVITY_ACCELERATION * slopeSin) / dragCoefficient);
// Uphill
if (slope > 0) { // First, calculate the final speed if we're going uphill
float time_to_peak = atan(starting_speed / terminal_velocity) / (drag_coefficient * terminal_velocity); if (slope > 0) {
// If the discriminant is greater than 1, the slope is so steep that we cannot reach the end with our starting speed float timeToPeak = atan(startingSpeed / terminalVelocity) / (dragCoefficient * terminalVelocity);
float discriminant = cos(drag_coefficient * terminal_velocity * time_to_peak) * exp(full_distance * drag_coefficient); float discriminant = exp(fullDistance * dragCoefficient) * cos(dragCoefficient * terminalVelocity * timeToPeak);
if (discriminant > 1.f) { if (discriminant > 1.f) {
return -1; // If this value is greater than 1, it means that the slope is too steep and we can never reach the top with our
} // starting speed
return -1;
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);
}
} }
return terminalVelocity * tan(acos(discriminant));
} }
if (final_speed < minimum_speed) { // Downhill must be split in three: starting slower than terminal velocity, starting at terminal velocity, and starting
return -1; // above terminal velocity
} else { if (fabs(startingSpeed - terminalVelocity) < 0.0001) {
return std::fmin(final_speed, maximum_speed); return terminalVelocity;
} }
// If we're going faster than terminal velocity
if (startingSpeed > terminalVelocity) {
float k1 = log((startingSpeed - terminalVelocity) / (startingSpeed + terminalVelocity));
float tanhInput = asinh(exp(dragCoefficient * fullDistance)*sinh(k1 * 0.5));
return -terminalVelocity / tanh(tanhInput);
}
// We only get here if we're going slower than terminal velocity
float k1 = log((terminalVelocity + startingSpeed) / (terminalVelocity - startingSpeed));
float tanhInput = acosh(exp(dragCoefficient * fullDistance) * cosh(k1 * 0.5));
return terminalVelocity * tanh(tanhInput);
}
float calculateRequiredSpeed(float endSpeed, float horizontalDistance, float heightDifference, float dragCoefficient) {
float slopeTan = heightDifference / horizontalDistance;
// If it's flat, the calculation is kinda simple
if (fabs(slopeTan) < 0.0001) {
return endSpeed * exp(horizontalDistance * dragCoefficient);
}
// If it's not flat, we're going to need the terminal velocity
float slope = atan(slopeTan);
float slopeSin = sin(slope);
float fullDistance = horizontalDistance * slopeTan / slopeSin;
float acceleration = -GRAVITY_ACCELERATION * slopeSin;
float terminalVelocity = sqrt(fabs(acceleration) / dragCoefficient);
// Uphill
if (slope > 0) {
float timeToPeak = acos(cos(atan(endSpeed / terminalVelocity))/exp(fullDistance * dragCoefficient)) / (terminalVelocity * dragCoefficient);
return terminalVelocity * tan(terminalVelocity * dragCoefficient * timeToPeak);
}
// Downhill, end speed equal to terminal velocity
if (fabs(endSpeed - terminalVelocity) < 0.001) {
return terminalVelocity;
}
// Downhill, end speed higher than terminal velocity
if (endSpeed > terminalVelocity) {
float k1 = asinh(exp(-fullDistance * dragCoefficient) * sinh(atanh(-terminalVelocity / endSpeed)));
return -terminalVelocity / tanh(k1);
}
// If we got here, we're going downhill with an end speed lower than terminal velocity
float discriminant = cosh(atanh(endSpeed / terminalVelocity)) / exp(fullDistance * dragCoefficient);
if (discriminant >= 1) {
return terminalVelocity * tanh(acosh(discriminant));
}
// If we got here, we will gain a higher speed than required even when starting from zero.
return 0;
} }
void getNeighbourConnections(RoadNode node, Connection* targetArray, int &numberOfConnections) { void getNeighbourConnections(RoadNode node, Connection* targetArray, int &numberOfConnections) {
numberOfConnections = 0; numberOfConnections = 0;
if (node.connection_one.connected_point_number != -1) { if (node.connectionOne.connectedPointNumber != -1) {
*(targetArray + numberOfConnections) = node.connection_one; *(targetArray + numberOfConnections) = node.connectionOne;
numberOfConnections++; numberOfConnections++;
} }
if (node.connection_two.connected_point_number != -1) { if (node.connectionTwo.connectedPointNumber != -1) {
*(targetArray + numberOfConnections) = node.connection_two; *(targetArray + numberOfConnections) = node.connectionTwo;
numberOfConnections++; numberOfConnections++;
} }
if (node.extra_connections != NULL) { if (node.extraConnections != NULL) {
for (auto it = node.extra_connections->begin(); it != node.extra_connections->end(); it++) { for (auto it = node.extraConnections->begin(); it != node.extraConnections->end(); it++) {
*(targetArray + numberOfConnections) = *it; *(targetArray + numberOfConnections) = *it;
numberOfConnections++; numberOfConnections++;
} }
} }
} }
SearchResult findAllPathsFromPoint(int startingNode, float minimum_speed, float maximum_speed, int maximumSpeedLimit, float drag_coefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway) { SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, bool reverse) {
SearchResult result; SearchResult result;
result.reverse = reverse;
result.startingNode = startingNode; result.startingNode = startingNode;
RoadNode firstNode = set.roadNodes[startingNode]; RoadNode firstNode = set.roadNodes[startingNode];
SearchNodeInfo firstNodeInfo; SearchNodeInfo firstNodeInfo;
firstNodeInfo.distanceFromPrevious = 0; firstNodeInfo.distanceFromPrevious = 0;
firstNodeInfo.currentSpeed = minimum_speed; firstNodeInfo.currentSpeed = minimumSpeed;
result.reachableNodes[startingNode] = firstNodeInfo; result.reachableNodes[startingNode] = firstNodeInfo;
ListNode *nextNode = new ListNode; ListNode *nextNode = new ListNode;
ListNode *lastNode = nextNode;
nextNode->id = startingNode; nextNode->id = startingNode;
nextNode->current_speed = minimum_speed; nextNode->currentSpeed = minimumSpeed;
nextNode->currentCourse = 0;
while (nextNode != NULL) { while (nextNode != NULL) {
ListNode *currentNode = nextNode; ListNode *currentNode = nextNode;
nextNode = currentNode->next; nextNode = currentNode->next;
int currentId = currentNode->id; int currentId = currentNode->id;
float currentSpeed = currentNode->current_speed; float currentSpeed = currentNode->currentSpeed;
float currentCourse = currentNode->currentCourse;
RoadNode bestNode = set.roadNodes[currentId]; RoadNode bestNode = set.roadNodes[currentId];
if (lastNode == currentNode) {
lastNode = NULL;
}
delete currentNode; delete currentNode;
Connection neighbours[10]; Connection neighbours[10];
@@ -339,11 +406,11 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimum_speed, float
for (int i = 0; i < neighbourCounter; i++) { for (int i = 0; i < neighbourCounter; i++) {
Connection neighbour = neighbours[i]; Connection neighbour = neighbours[i];
// First, if neighbour is in excluded area, skip it // First, if neighbour is in excluded area, skip it
if (excludedNodes.find(neighbour.connected_point_number) != excludedNodes.end()) { if (excludedNodes.find(neighbour.connectedPointNumber) != excludedNodes.end()) {
continue; continue;
} }
if (neighbour.speed_limit > maximumSpeedLimit) { if (neighbour.speedLimit > maximumSpeedLimit) {
continue; continue;
} }
@@ -355,49 +422,116 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimum_speed, float
continue; continue;
} }
if (neighbour.againstOneWay && ! allowAgainstOneway) { bool againstOneWay;
if (reverse) {
againstOneWay = !neighbour.passableOppositeDirection;
} else {
againstOneWay = !neighbour.passableSameDirection;
}
if (!allowAgainstOneway && againstOneWay) {
continue; continue;
} }
RoadNode neighbourNode = set.roadNodes[neighbour.connected_point_number]; RoadNode neighbourNode = set.roadNodes[neighbour.connectedPointNumber];
float heightDifference = neighbourNode.position_z - bestNode.position_z; float heightDifference = neighbourNode.positionZ - bestNode.positionZ;
float resultingSpeed = calculate_speed(currentSpeed, neighbour.distance, heightDifference, minimum_speed, maximum_speed, drag_coefficient); float resultingSpeed = -1;
if (!reverse) {
if (resultingSpeed < 0) { resultingSpeed = calculateSpeed(currentSpeed, neighbour.distance, heightDifference, dragCoefficient);
continue; if (resultingSpeed < minimumSpeed) {
continue;
}
resultingSpeed = fmin(resultingSpeed, maximumSpeed);
} else {
resultingSpeed = calculateRequiredSpeed(currentSpeed, neighbour.distance, -heightDifference, dragCoefficient);
if (resultingSpeed > maximumSpeed) {
continue;
}
resultingSpeed = fmax(resultingSpeed, minimumSpeed);
}
// If we limit the speed on corners, do that here
if (limitCornerSpeed) {
float courseDifference = fabs(currentCourse - neighbour.course);
if (courseDifference > 180.0) {
courseDifference = 360 - courseDifference;
}
float maximumCornerSpeed;
if (courseDifference > 95) {
maximumCornerSpeed = minimumSpeed;
} else if (courseDifference > 45.0) {
maximumCornerSpeed = (95 - courseDifference) / 50.0 * (maximumSpeed - minimumSpeed) + minimumSpeed;
} else {
maximumCornerSpeed = maximumSpeed;
}
if (reverse) {
if (resultingSpeed > maximumCornerSpeed) {
continue;
}
} else {
resultingSpeed = fmin(maximumCornerSpeed, resultingSpeed);
}
} }
// Check if this node is already in the reachable nodes map // Check if this node is already in the reachable nodes map
auto resultIterator = result.reachableNodes.find(neighbour.connected_point_number); auto resultIterator = result.reachableNodes.find(neighbour.connectedPointNumber);
if (resultIterator != result.reachableNodes.end() && resultingSpeed <= resultIterator->second.currentSpeed) { if (reverse) {
continue; if (resultIterator != result.reachableNodes.end() && resultingSpeed >= resultIterator->second.currentSpeed) {
continue;
}
} else {
if (resultIterator != result.reachableNodes.end() && resultingSpeed <= resultIterator->second.currentSpeed) {
continue;
}
} }
SearchNodeInfo reachableNodeInfo; SearchNodeInfo reachableNodeInfo;
reachableNodeInfo.currentSpeed = resultingSpeed; reachableNodeInfo.currentSpeed = resultingSpeed;
reachableNodeInfo.distanceFromPrevious = neighbour.distance; reachableNodeInfo.distanceFromPrevious = neighbour.distance;
result.reachableNodes[neighbour.connected_point_number] = reachableNodeInfo; result.reachableNodes[neighbour.connectedPointNumber] = reachableNodeInfo;
result.previous[neighbour.connected_point_number] = currentId; result.previous[neighbour.connectedPointNumber] = currentId;
ListNode *neighbourListNode = new ListNode; ListNode *neighbourListNode = new ListNode;
neighbourListNode->id = neighbour.connected_point_number; neighbourListNode->id = neighbour.connectedPointNumber;
neighbourListNode->current_speed = reachableNodeInfo.currentSpeed; neighbourListNode->currentSpeed = reachableNodeInfo.currentSpeed;
neighbourListNode->currentCourse = neighbour.course;
if (nextNode == NULL || resultingSpeed < nextNode->current_speed) { if (reverse) {
neighbourListNode->next = nextNode; if (nextNode == NULL || resultingSpeed > nextNode->currentSpeed) {
nextNode = neighbourListNode; neighbourListNode->next = nextNode;
nextNode = neighbourListNode;
} else {
ListNode* previousSearchNode = nextNode;
ListNode* currentSearchNode = nextNode->next;
while (currentSearchNode != NULL && currentSearchNode->currentSpeed < resultingSpeed) {
previousSearchNode = currentSearchNode;
currentSearchNode = currentSearchNode->next;
}
previousSearchNode->next = neighbourListNode;
neighbourListNode->next = currentSearchNode;
}
} else { } else {
ListNode* previousSearchNode = nextNode; if (nextNode == NULL || resultingSpeed < nextNode->currentSpeed) {
ListNode* currentSearchNode = nextNode->next; neighbourListNode->next = nextNode;
nextNode = neighbourListNode;
while(currentSearchNode != NULL && currentSearchNode->current_speed > resultingSpeed) { } else {
previousSearchNode = currentSearchNode; ListNode* previousSearchNode = nextNode;
currentSearchNode = currentSearchNode->next; ListNode* currentSearchNode = nextNode->next;
while(currentSearchNode != NULL && currentSearchNode->currentSpeed > resultingSpeed) {
previousSearchNode = currentSearchNode;
currentSearchNode = currentSearchNode->next;
}
previousSearchNode->next = neighbourListNode;
neighbourListNode->next = currentSearchNode;
} }
previousSearchNode->next = neighbourListNode;
neighbourListNode->next = currentSearchNode;
} }
} }
} }
@@ -405,11 +539,11 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimum_speed, float
return result; return result;
} }
JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway) { JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, bool reverse) {
lastSearchResult = findAllPathsFromPoint(startingNode, minimumSpeed, maximumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway); lastSearchResult = findAllPathsFromPoint(startingNode, minimumSpeed, maximumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, limitCornerSpeed, reverse);
float start_x = set.roadNodes[startingNode].position_x; float startX = set.roadNodes[startingNode].positionX;
float start_y = set.roadNodes[startingNode].position_y; float startY = set.roadNodes[startingNode].positionY;
// Find all end points and sort them by distance // Find all end points and sort them by distance
std::set<uint32_t> notEndPoints; std::set<uint32_t> notEndPoints;
@@ -422,9 +556,9 @@ JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, flo
if (notEndPoints.find(it->first) == notEndPoints.end()) { if (notEndPoints.find(it->first) == notEndPoints.end()) {
JSNodeInfo entry; JSNodeInfo entry;
entry.nodeId = it->first; entry.nodeId = it->first;
entry.positionX = set.roadNodes[entry.nodeId].position_x; entry.positionX = set.roadNodes[entry.nodeId].positionX;
entry.positionY = set.roadNodes[entry.nodeId].position_y; entry.positionY = set.roadNodes[entry.nodeId].positionY;
entry.distanceFromStart = sqrt(pow(entry.positionX - start_x, 2) + pow(entry.positionY - start_y, 2)); entry.distanceFromStart = sqrt(pow(entry.positionX - startX, 2) + pow(entry.positionY - startY, 2));
farthestEndpoints.push_back(entry); farthestEndpoints.push_back(entry);
} }
@@ -457,50 +591,10 @@ JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, flo
JSSearchResult searchResult; JSSearchResult searchResult;
searchResult.endPoints = filteredEndpoints; searchResult.endPoints = filteredEndpoints;
searchResult.reverse = lastSearchResult.reverse;
return searchResult; return searchResult;
} }
float calculateRequiredSpeed(float endSpeed, float horizontalDistance, float heightDifference, float dragCoefficient) {
// The bike will never reach faster speeds than terminal velocity in free fall
float maximumSpeed = sqrt(GRAVITY_ACCELERATION / dragCoefficient);
// First, check if we'll reach the required speed no matter what
if (calculate_speed(0.00001, horizontalDistance, heightDifference, 0.0, maximumSpeed, dragCoefficient) >= endSpeed) {
return 0.0;
}
// Divide and conquer
float numerator = 0;
float denominator = 0.5;
float foundEndSpeed = -100.0;
int loopCounter = 0;
float closestFoundSpeed = 1e99;
float bestNumerator = 1;
float bestDenominator = 1;
do {
numerator *= 2;
denominator *= 2;
if (foundEndSpeed > endSpeed) {
numerator -= 1;
} else {
numerator += 1;
}
foundEndSpeed = calculate_speed(maximumSpeed * numerator / denominator, horizontalDistance, heightDifference, 0.01, 100.0, dragCoefficient);
loopCounter++;
if (fabs(foundEndSpeed - endSpeed) < closestFoundSpeed) {
closestFoundSpeed = foundEndSpeed;
bestNumerator = numerator;
bestDenominator = denominator;
}
} while (fabs(foundEndSpeed - endSpeed) > 0.013 && loopCounter < 32);
float requiredSpeed = maximumSpeed * bestNumerator / bestDenominator;
return requiredSpeed;
}
std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float minimumSpeed, float dragCoefficient) { std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float minimumSpeed, float dragCoefficient) {
std::vector<JSNodeInfo> path; std::vector<JSNodeInfo> path;
@@ -526,9 +620,9 @@ std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float
RoadNode roadNode = set.roadNodes[*it]; RoadNode roadNode = set.roadNodes[*it];
JSNodeInfo nodeInfo; JSNodeInfo nodeInfo;
nodeInfo.nodeId = *it; nodeInfo.nodeId = *it;
nodeInfo.positionX = roadNode.position_x; nodeInfo.positionX = roadNode.positionX;
nodeInfo.positionY = roadNode.position_y; nodeInfo.positionY = roadNode.positionY;
nodeInfo.positionZ = roadNode.position_z; nodeInfo.positionZ = roadNode.positionZ;
SearchNodeInfo searchNodeInfo = lastSearchResult.reachableNodes.find(*it)->second; SearchNodeInfo searchNodeInfo = lastSearchResult.reachableNodes.find(*it)->second;
nodeInfo.currentSpeed = searchNodeInfo.currentSpeed; nodeInfo.currentSpeed = searchNodeInfo.currentSpeed;
@@ -538,6 +632,10 @@ std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float
path.push_back(nodeInfo); path.push_back(nodeInfo);
} }
if (lastSearchResult.reverse) {
std::reverse(path.begin(), path.end());
}
float currentRequiredSpeed = -1.0; float currentRequiredSpeed = -1.0;
for (auto it = path.rbegin(); it != path.rend(); it++) { for (auto it = path.rbegin(); it != path.rend(); it++) {
if (currentRequiredSpeed <= -1.0) { if (currentRequiredSpeed <= -1.0) {
@@ -549,14 +647,14 @@ std::vector<JSNodeInfo> getPathJS(uint32_t startingNode, uint32_t endNode, float
uint32_t nextNodeId = (it - 1)->nodeId; uint32_t nextNodeId = (it - 1)->nodeId;
RoadNode nextNode = set.roadNodes[nextNodeId]; RoadNode nextNode = set.roadNodes[nextNodeId];
float heightDifference = nextNode.position_z - currentNode.position_z; float heightDifference = nextNode.positionZ - currentNode.positionZ;
Connection neighbours[10]; Connection neighbours[10];
int numberOfNeighbours = 0; int numberOfNeighbours = 0;
getNeighbourConnections(currentNode, neighbours, numberOfNeighbours); getNeighbourConnections(currentNode, neighbours, numberOfNeighbours);
for (int i = 0; i < numberOfNeighbours; i++) { for (int i = 0; i < numberOfNeighbours; i++) {
Connection neighbour = neighbours[i]; Connection neighbour = neighbours[i];
if (neighbour.connected_point_number == nextNodeId) { if (neighbour.connectedPointNumber == nextNodeId) {
float horizontalDistance = neighbour.distance; float horizontalDistance = neighbour.distance;
currentRequiredSpeed = fmax(calculateRequiredSpeed(currentRequiredSpeed, horizontalDistance, heightDifference, dragCoefficient), minimumSpeed); currentRequiredSpeed = fmax(calculateRequiredSpeed(currentRequiredSpeed, horizontalDistance, heightDifference, dragCoefficient), minimumSpeed);
it->requiredSpeed = currentRequiredSpeed; it->requiredSpeed = currentRequiredSpeed;
@@ -634,13 +732,13 @@ void getNodesWithinPolygons(std::vector<std::vector<std::vector<PolygonCoordinat
for (size_t nodeId = 0; nodeId < set.numberOfNodes; nodeId++) { for (size_t nodeId = 0; nodeId < set.numberOfNodes; nodeId++) {
RoadNode node = set.roadNodes[nodeId]; RoadNode node = set.roadNodes[nodeId];
// If the node is outside the bounding box, just move on // 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) { if (node.positionX < minX || node.positionX > maxX || node.positionY < minY || node.positionY > maxY) {
continue; continue;
} }
// Otherwise, count how many times a ray straight east from the point crosses the polygon's rings // Otherwise, count how many times a ray straight east from the point crosses the polygon's rings
int crossings = 0; int crossings = 0;
for (auto ring : polygon) { for (auto ring : polygon) {
if (isInsideRing(node.position_x, node.position_y, ring)) { if (isInsideRing(node.positionX, node.positionY, ring)) {
crossings += 1; crossings += 1;
} }
} }
@@ -681,7 +779,7 @@ std::vector<uint32_t> findPossibleStartNodes(float minimumSpeed, float maximumSp
for (int i = 0; i < numberOfNeighbours; i++) { for (int i = 0; i < numberOfNeighbours; i++) {
Connection connection = neighbours[i]; Connection connection = neighbours[i];
if (excludedNodes.find(connection.connected_point_number) != excludedNodes.end()) { if (excludedNodes.find(connection.connectedPointNumber) != excludedNodes.end()) {
continue; continue;
} }
if (connection.motorway && !allowMotorways) { if (connection.motorway && !allowMotorways) {
@@ -690,12 +788,12 @@ std::vector<uint32_t> findPossibleStartNodes(float minimumSpeed, float maximumSp
if (connection.tunnel && !allowTunnels) { if (connection.tunnel && !allowTunnels) {
continue; continue;
} }
if (connection.againstOneWay && !allowAgainstOneway) { if (!connection.passableSameDirection && !allowAgainstOneway) {
continue; continue;
} }
RoadNode neighbourNode = set.roadNodes[connection.connected_point_number]; RoadNode neighbourNode = set.roadNodes[connection.connectedPointNumber];
float slopeTan = (neighbourNode.position_z - node.position_z) / connection.distance; float slopeTan = (neighbourNode.positionZ - node.positionZ) / connection.distance;
if (slopeTan > minimumSlopeTan) { if (slopeTan > minimumSlopeTan) {
hasWayIn = true; hasWayIn = true;
break; break;
@@ -711,7 +809,7 @@ std::vector<uint32_t> findPossibleStartNodes(float minimumSpeed, float maximumSp
possibleStartNodes.begin(), possibleStartNodes.begin(),
possibleStartNodes.end(), possibleStartNodes.end(),
nodeId, nodeId,
[](uint32_t nodeOne, uint32_t nodeTwo){return set.roadNodes[nodeOne].position_z > set.roadNodes[nodeTwo].position_z;} [](uint32_t nodeOne, uint32_t nodeTwo){return set.roadNodes[nodeOne].positionZ > set.roadNodes[nodeTwo].positionZ;}
), ),
nodeId nodeId
); );
@@ -721,7 +819,7 @@ std::vector<uint32_t> findPossibleStartNodes(float minimumSpeed, float maximumSp
return possibleStartNodes; return possibleStartNodes;
} }
AreaSearchResult startAreaSearch(float minimumSpeed, float maximumSpeed, float maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, std::vector<std::vector<std::vector<PolygonCoordinate>>> searchArea) { AreaSearchResult startAreaSearch(float minimumSpeed, float maximumSpeed, float maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, std::vector<std::vector<std::vector<PolygonCoordinate>>> searchArea) {
currentAreaSearch = CurrentAreaSearch(); currentAreaSearch = CurrentAreaSearch();
currentAreaSearch.startNodes = findPossibleStartNodes(minimumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, searchArea); currentAreaSearch.startNodes = findPossibleStartNodes(minimumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, searchArea);
currentAreaSearch.minimumSpeed = minimumSpeed; currentAreaSearch.minimumSpeed = minimumSpeed;
@@ -731,6 +829,7 @@ AreaSearchResult startAreaSearch(float minimumSpeed, float maximumSpeed, float m
currentAreaSearch.allowMotorways = allowMotorways; currentAreaSearch.allowMotorways = allowMotorways;
currentAreaSearch.allowTunnels = allowTunnels; currentAreaSearch.allowTunnels = allowTunnels;
currentAreaSearch.allowAgainstOneway = allowAgainstOneway; currentAreaSearch.allowAgainstOneway = allowAgainstOneway;
currentAreaSearch.limitCornerSpeed = limitCornerSpeed;
AreaSearchResult result; AreaSearchResult result;
result.remainingNodes = currentAreaSearch.startNodes.size(); result.remainingNodes = currentAreaSearch.startNodes.size();
@@ -747,7 +846,9 @@ AreaSearchResult continueAreaSearch() {
currentAreaSearch.dragCoefficient, currentAreaSearch.dragCoefficient,
currentAreaSearch.allowMotorways, currentAreaSearch.allowMotorways,
currentAreaSearch.allowTunnels, currentAreaSearch.allowTunnels,
currentAreaSearch.allowAgainstOneway currentAreaSearch.allowAgainstOneway,
currentAreaSearch.limitCornerSpeed,
false
); );
// Remove all nodes we have reached from here as possible future start nodes // Remove all nodes we have reached from here as possible future start nodes
@@ -773,7 +874,7 @@ AreaSearchResult continueAreaSearch() {
for (auto it = result.reachableNodes.begin(); it != result.reachableNodes.end(); it++) { for (auto it = result.reachableNodes.begin(); it != result.reachableNodes.end(); it++) {
if (notEndPoints.find(it->first) == notEndPoints.end()) { if (notEndPoints.find(it->first) == notEndPoints.end()) {
RoadNode endNode = set.roadNodes[it->first]; 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)); float distance = sqrt(pow(endNode.positionX - startNode.positionX, 2) + pow( endNode.positionY - startNode.positionY, 2));
if (distance > farthestDistance) { if (distance > farthestDistance) {
farthestDistance = distance; farthestDistance = distance;
farthestNode = it->first; farthestNode = it->first;
@@ -781,22 +882,76 @@ AreaSearchResult continueAreaSearch() {
} }
} }
std::set<uint32_t> path; std::map<uint32_t, std::vector<uint32_t>> path;
path.insert(farthestNode); path[farthestNode] = std::vector<uint32_t>();
uint32_t currentNode = farthestNode; uint32_t currentNode = farthestNode;
while (result.previous.find(currentNode) != result.previous.end()) { while (result.previous.find(currentNode) != result.previous.end()) {
currentNode = result.previous.find(currentNode)->second; currentNode = result.previous.find(currentNode)->second;
path.insert(currentNode); path[currentNode] = std::vector<uint32_t>();
} }
// Check if our path overlaps too much with already travelled paths // Check if our path overlaps too much with already travelled paths
std::vector<uint32_t> overlap; std::map<uint32_t, std::vector<uint32_t>> overlap;
std::set_intersection(path.begin(), path.end(), currentAreaSearch.utilizedNodes.begin(), currentAreaSearch.utilizedNodes.end(), std::back_inserter(overlap)); auto inserterIterator = overlap.begin();
if (overlap.size() < 0.5 * path.size()) { std::set_intersection(
currentAreaSearch.utilizedNodes.begin(),
currentAreaSearch.utilizedNodes.end(),
path.begin(),
path.end(),
std::inserter(overlap, inserterIterator),
currentAreaSearch.utilizedNodes.value_comp()
);
std::map<uint32_t, uint32_t> overlapCounts;
for (auto it = overlap.begin(); it != overlap.end(); it++) {
for (auto startingNodeIt = it->second.begin(); startingNodeIt != it->second.end(); startingNodeIt++) {
uint32_t startingNode = *startingNodeIt;
if (overlapCounts.find(startingNode) == overlapCounts.end()) {
overlapCounts[startingNode] = 0;
}
overlapCounts[startingNode] = overlapCounts[startingNode] + 1;
}
}
auto overlapIterator = overlapCounts.begin();
while (overlapIterator != overlapCounts.end()) {
uint32_t overlapCount = overlapIterator->second;
if (overlapCount < path.size() * 0.5) {
overlapIterator = overlapCounts.erase(overlapIterator);
continue;
}
// In case we overlap more than 50% with another path, check if the current path is longer
uint32_t otherPath = overlapIterator->first;
bool isLongerThanOtherPath = true;
for (auto otherPathIterator = currentAreaSearch.currentAreaSearchResults.begin(); otherPathIterator != currentAreaSearch.currentAreaSearchResults.end(); otherPathIterator++) {
if (otherPathIterator->nodeId != otherPath) {
continue;
}
if (farthestDistance > otherPathIterator->longestRoute) {
currentAreaSearch.currentAreaSearchResults.erase(otherPathIterator);
break;
} else {
isLongerThanOtherPath = false;
break;
}
}
if (isLongerThanOtherPath) {
overlapIterator = overlapCounts.erase(overlapIterator);
continue;
}
// If we got here, we found another path that shares at least 50% of or route points, that is longer than the current route
break;
}
if (overlapCounts.size() == 0) {
AreaSearchEntry searchEntry; AreaSearchEntry searchEntry;
searchEntry.nodeId = currentStartNode; searchEntry.nodeId = currentStartNode;
searchEntry.positionX = startNode.position_x; searchEntry.positionX = startNode.positionX;
searchEntry.positionY = startNode.position_y; searchEntry.positionY = startNode.positionY;
searchEntry.longestRoute = farthestDistance; searchEntry.longestRoute = farthestDistance;
currentAreaSearch.currentAreaSearchResults.insert( currentAreaSearch.currentAreaSearchResults.insert(
@@ -809,8 +964,12 @@ AreaSearchResult continueAreaSearch() {
searchEntry searchEntry
); );
for (uint32_t pathNode : path) { for (std::pair<uint32_t, std::vector<uint32_t>> pathNodeEntry : path) {
currentAreaSearch.utilizedNodes.insert(pathNode); uint32_t pathNode = pathNodeEntry.first;
if (currentAreaSearch.utilizedNodes.find(pathNode) == currentAreaSearch.utilizedNodes.end()) {
currentAreaSearch.utilizedNodes[pathNode] = std::vector<uint32_t>();
}
currentAreaSearch.utilizedNodes[pathNode].push_back(currentStartNode);
} }
} }
@@ -823,6 +982,7 @@ AreaSearchResult continueAreaSearch() {
return searchResult; return searchResult;
} }
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_BINDINGS(my_module) { EMSCRIPTEN_BINDINGS(my_module) {
emscripten::class_<JSNodeInfo>("NodeInfo") emscripten::class_<JSNodeInfo>("NodeInfo")
.constructor<>() .constructor<>()
@@ -836,7 +996,8 @@ EMSCRIPTEN_BINDINGS(my_module) {
emscripten::class_<JSSearchResult>("SearchResult") emscripten::class_<JSSearchResult>("SearchResult")
.constructor<>() .constructor<>()
.property("endPoints", &JSSearchResult::endPoints); .property("endPoints", &JSSearchResult::endPoints)
.property("reverse", &JSSearchResult::reverse);
emscripten::class_<PolygonCoordinate>("PolygonCoordinate") emscripten::class_<PolygonCoordinate>("PolygonCoordinate")
.constructor<>() .constructor<>()
@@ -862,6 +1023,7 @@ EMSCRIPTEN_BINDINGS(my_module) {
emscripten::register_vector<AreaSearchEntry>("AreaSearchEntries"); emscripten::register_vector<AreaSearchEntry>("AreaSearchEntries");
emscripten::function("loadData", &loadData); emscripten::function("loadData", &loadData);
emscripten::function("getEpsgCode", &getEpsgCode);
emscripten::function("findClosestNode", &findClosestNode); emscripten::function("findClosestNode", &findClosestNode);
emscripten::function("findAllPathsFromPoint", &findAllPathsFromPointJS); emscripten::function("findAllPathsFromPoint", &findAllPathsFromPointJS);
emscripten::function("getPath", &getPathJS); emscripten::function("getPath", &getPathJS);
@@ -869,3 +1031,4 @@ EMSCRIPTEN_BINDINGS(my_module) {
emscripten::function("startAreaSearch", &startAreaSearch); emscripten::function("startAreaSearch", &startAreaSearch);
emscripten::function("continueAreaSearch", &continueAreaSearch); emscripten::function("continueAreaSearch", &continueAreaSearch);
} }
#endif
+7 -3
View File
@@ -44,7 +44,9 @@ interface FindPathsFromNode {
dragCoefficient: number, dragCoefficient: number,
allowMotorways: boolean, allowMotorways: boolean,
allowTunnels: boolean, allowTunnels: boolean,
allowAgainstOneway: boolean allowAgainstOneway: boolean,
limitCornerSpeed: boolean,
reverse: boolean
} }
export interface Endpoint { export interface Endpoint {
@@ -56,7 +58,8 @@ export interface Endpoint {
interface FoundPathsFromNode { interface FoundPathsFromNode {
nodeId: number, nodeId: number,
endpoints: Endpoint[] endpoints: Endpoint[],
reverse: boolean
} }
interface GetFullPath { interface GetFullPath {
@@ -102,7 +105,8 @@ interface SearchArea {
dragCoefficient: number, dragCoefficient: number,
allowMotorways: boolean, allowMotorways: boolean,
allowTunnels: boolean, allowTunnels: boolean,
allowAgainstOneway: boolean allowAgainstOneway: boolean,
limitCornerSpeed: boolean
} }
interface ContinueSearch { interface ContinueSearch {
+31 -12
View File
@@ -2,7 +2,7 @@ import './style.css';
import './modules/maphandler/maphandler' import './modules/maphandler/maphandler'
import MapHandler from './modules/maphandler/maphandler'; import MapHandler from './modules/maphandler/maphandler';
import type { Message, PathSegment, Polygon } from './interfaces'; import type { Message, PathSegment, Polygon } from './interfaces';
import { downloadGpxFile } from './modules/gpx/exporter'; import { downloadKmlFile } from './modules/kml/exporter';
interface WindowState { interface WindowState {
state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching' state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching'
@@ -16,7 +16,9 @@ interface Settings {
allowMotorways: boolean, allowMotorways: boolean,
allowTunnels: boolean, allowTunnels: boolean,
allowAgainstOneway: boolean, allowAgainstOneway: boolean,
cutoffDistance: number limitCornerSpeed: boolean,
cutoffDistance: number,
reverse: boolean
} }
// Default settings values // Default settings values
@@ -27,6 +29,7 @@ const DEFAULT_DRAG_COEFFICIENT = 0.005;
const DEFAULT_ALLOW_MOTORWAYS = false; const DEFAULT_ALLOW_MOTORWAYS = false;
const DEFAULT_ALLOW_TUNNELS = false; const DEFAULT_ALLOW_TUNNELS = false;
const DEFAULT_ALLOW_AGAINST_ONE_WAY = false; const DEFAULT_ALLOW_AGAINST_ONE_WAY = false;
const DEFAULT_LIMIT_CORNER_SPEED = true;
const DEFAULT_CUTOFF_DISTANCE = 1000; const DEFAULT_CUTOFF_DISTANCE = 1000;
// Set up variables // Set up variables
@@ -45,6 +48,7 @@ let loadingContainer = document.getElementById('loadingcontainer');
let mapContainer = document.getElementById('mapcontainer'); let mapContainer = document.getElementById('mapcontainer');
let searchButton = document.getElementById('search-button'); let searchButton = document.getElementById('search-button');
let exportButton = document.getElementById('export-route-button'); let exportButton = document.getElementById('export-route-button');
let reverseCheckbox = document.getElementById('reverse-search-input');
let searchStatusParagraph = document.getElementById('search-status-paragraph'); let searchStatusParagraph = document.getElementById('search-status-paragraph');
let searchResultsTable = document.getElementById('search-result-table'); let searchResultsTable = document.getElementById('search-result-table');
let searchResultTableBody = document.getElementById('search-result-table-body'); let searchResultTableBody = document.getElementById('search-result-table-body');
@@ -58,6 +62,7 @@ let dragCoefficientInput = document.getElementById('drag-coefficient-input');
let allowMotorwaysInput = document.getElementById('allow-motorways-input'); let allowMotorwaysInput = document.getElementById('allow-motorways-input');
let allowTunnelsInput = document.getElementById('allow-tunnels-input'); let allowTunnelsInput = document.getElementById('allow-tunnels-input');
let allowAgainstOnewayInput = document.getElementById('allow-wrong-way-input'); let allowAgainstOnewayInput = document.getElementById('allow-wrong-way-input');
let limitCornerSpeedInput = document.getElementById('limit-corner-speed-input');
let cutoffDistanceInput = document.getElementById('cutoff-distance-input'); let cutoffDistanceInput = document.getElementById('cutoff-distance-input');
@@ -69,8 +74,8 @@ routeWorker.onmessage = e => {
} }
else if (message.foundClosestNode != null) { else if (message.foundClosestNode != null) {
mapHandler?.drawStartNode(message.foundClosestNode.foundNodeId, message.foundClosestNode.foundLatitude, message.foundClosestNode.foundLongitude);
let settings = getSettings(); let settings = getSettings();
mapHandler?.drawStartNode(message.foundClosestNode.foundNodeId, message.foundClosestNode.foundLatitude, message.foundClosestNode.foundLongitude, settings.reverse);
let findPathsMessage: Message = {findPathsFromNode: { let findPathsMessage: Message = {findPathsFromNode: {
nodeId: message.foundClosestNode.foundNodeId, nodeId: message.foundClosestNode.foundNodeId,
minimumSpeed: settings.minimumSpeed, minimumSpeed: settings.minimumSpeed,
@@ -79,11 +84,13 @@ routeWorker.onmessage = e => {
dragCoefficient: settings.dragCoefficient, dragCoefficient: settings.dragCoefficient,
allowMotorways: settings.allowMotorways, allowMotorways: settings.allowMotorways,
allowTunnels: settings.allowTunnels, allowTunnels: settings.allowTunnels,
allowAgainstOneway: settings.allowAgainstOneway allowAgainstOneway: settings.allowAgainstOneway,
limitCornerSpeed: settings.limitCornerSpeed,
reverse: settings.reverse
}}; }};
routeWorker.postMessage(findPathsMessage); routeWorker.postMessage(findPathsMessage);
} else if (message.foundPathsFromNode != null) { } else if (message.foundPathsFromNode != null) {
mapHandler?.drawEndPoints(message.foundPathsFromNode.endpoints); mapHandler?.drawEndPoints(message.foundPathsFromNode.endpoints, message.foundPathsFromNode.reverse);
} else if (message.returnFullPath != null) { } else if (message.returnFullPath != null) {
let settings = getSettings(); let settings = getSettings();
mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed); mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed);
@@ -118,13 +125,15 @@ routeWorker.onmessage = e => {
latitudeCell.setHTMLUnsafe(result.latitude.toFixed(6)); latitudeCell.setHTMLUnsafe(result.latitude.toFixed(6));
longitudeCell.setHTMLUnsafe(result.longitude.toFixed(6)); longitudeCell.setHTMLUnsafe(result.longitude.toFixed(6));
latitudeCell.classList.add('hidable');
longitudeCell.classList.add('hidable');
let button = document.createElement('button'); let button = document.createElement('button');
buttonCell.appendChild(button); buttonCell.appendChild(button);
button.setHTMLUnsafe('Show in map'); button.setHTMLUnsafe('Show in map');
button.addEventListener('click', _ => { button.addEventListener('click', _ => {
mapHandler?.drawStartNode(result.nodeId, result.latitude, result.longitude); mapHandler?.drawStartNode(result.nodeId, result.latitude, result.longitude, false);
let settings = getSettings();
let requestMessage: Message = { let requestMessage: Message = {
findPathsFromNode: { findPathsFromNode: {
nodeId: result.nodeId, nodeId: result.nodeId,
@@ -134,7 +143,9 @@ routeWorker.onmessage = e => {
dragCoefficient: settings.dragCoefficient, dragCoefficient: settings.dragCoefficient,
allowMotorways: settings.allowMotorways, allowMotorways: settings.allowMotorways,
allowTunnels: settings.allowTunnels, allowTunnels: settings.allowTunnels,
allowAgainstOneway: settings.allowAgainstOneway allowAgainstOneway: settings.allowAgainstOneway,
limitCornerSpeed: settings.limitCornerSpeed,
reverse: false
} }
}; };
routeWorker.postMessage(requestMessage); routeWorker.postMessage(requestMessage);
@@ -186,7 +197,9 @@ function setUpMapHandler() {
dragCoefficient: settings.dragCoefficient, dragCoefficient: settings.dragCoefficient,
allowMotorways: settings.allowMotorways, allowMotorways: settings.allowMotorways,
allowTunnels: settings.allowTunnels, allowTunnels: settings.allowTunnels,
allowAgainstOneway: settings.allowAgainstOneway allowAgainstOneway: settings.allowAgainstOneway,
limitCornerSpeed: settings.limitCornerSpeed,
reverse: settings.reverse
}}; }};
routeWorker.postMessage(newRoutesMessage); routeWorker.postMessage(newRoutesMessage);
} }
@@ -303,6 +316,7 @@ setUpNumberInput(dragCoefficientInput, 'drag-coefficient', DEFAULT_DRAG_COEFFICI
setUpBooleanInput(allowMotorwaysInput, 'allow-motorways', DEFAULT_ALLOW_MOTORWAYS); setUpBooleanInput(allowMotorwaysInput, 'allow-motorways', DEFAULT_ALLOW_MOTORWAYS);
setUpBooleanInput(allowTunnelsInput, 'allow-tunnels', DEFAULT_ALLOW_TUNNELS); setUpBooleanInput(allowTunnelsInput, 'allow-tunnels', DEFAULT_ALLOW_TUNNELS);
setUpBooleanInput(allowAgainstOnewayInput, 'allow-against-one-way', DEFAULT_ALLOW_AGAINST_ONE_WAY); setUpBooleanInput(allowAgainstOnewayInput, 'allow-against-one-way', DEFAULT_ALLOW_AGAINST_ONE_WAY);
setUpBooleanInput(limitCornerSpeedInput, 'limit-corner-speed', DEFAULT_LIMIT_CORNER_SPEED);
setUpNumberInput(cutoffDistanceInput, 'cutoff-distance', DEFAULT_CUTOFF_DISTANCE); setUpNumberInput(cutoffDistanceInput, 'cutoff-distance', DEFAULT_CUTOFF_DISTANCE);
function getSettings(): Settings { function getSettings(): Settings {
@@ -314,7 +328,9 @@ function getSettings(): Settings {
allowMotorways: getBooleanValue(allowMotorwaysInput, DEFAULT_ALLOW_MOTORWAYS), allowMotorways: getBooleanValue(allowMotorwaysInput, DEFAULT_ALLOW_MOTORWAYS),
allowTunnels: getBooleanValue(allowTunnelsInput, DEFAULT_ALLOW_TUNNELS), allowTunnels: getBooleanValue(allowTunnelsInput, DEFAULT_ALLOW_TUNNELS),
allowAgainstOneway: getBooleanValue(allowAgainstOnewayInput, DEFAULT_ALLOW_AGAINST_ONE_WAY), allowAgainstOneway: getBooleanValue(allowAgainstOnewayInput, DEFAULT_ALLOW_AGAINST_ONE_WAY),
cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE) limitCornerSpeed: getBooleanValue(limitCornerSpeedInput, DEFAULT_LIMIT_CORNER_SPEED),
cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE),
reverse: getBooleanValue(reverseCheckbox, false)
}; };
} }
@@ -326,6 +342,7 @@ function enableSettings(): void {
enableInput(allowMotorwaysInput); enableInput(allowMotorwaysInput);
enableInput(allowTunnelsInput); enableInput(allowTunnelsInput);
enableInput(allowAgainstOnewayInput); enableInput(allowAgainstOnewayInput);
enableInput(limitCornerSpeedInput);
} }
function disableSettings(): void { function disableSettings(): void {
@@ -336,6 +353,7 @@ function disableSettings(): void {
disableInput(allowMotorwaysInput); disableInput(allowMotorwaysInput);
disableInput(allowTunnelsInput); disableInput(allowTunnelsInput);
disableInput(allowAgainstOnewayInput); disableInput(allowAgainstOnewayInput);
disableInput(limitCornerSpeedInput);
} }
@@ -353,7 +371,8 @@ searchButton?.addEventListener('click', _ => {
dragCoefficient: settings.dragCoefficient, dragCoefficient: settings.dragCoefficient,
allowMotorways: settings.allowMotorways, allowMotorways: settings.allowMotorways,
allowTunnels: settings.allowTunnels, allowTunnels: settings.allowTunnels,
allowAgainstOneway: settings.allowAgainstOneway allowAgainstOneway: settings.allowAgainstOneway,
limitCornerSpeed: settings.limitCornerSpeed
}}; }};
routeWorker.postMessage(message); routeWorker.postMessage(message);
} }
@@ -364,7 +383,7 @@ searchButton?.addEventListener('click', _ => {
exportButton?.addEventListener('click', _ => { exportButton?.addEventListener('click', _ => {
if (lastRoute != null) { if (lastRoute != null) {
downloadGpxFile(lastRoute); downloadKmlFile(lastRoute);
} }
}) })
-122
View File
@@ -1,122 +0,0 @@
import type { Coordinate, PathSegment } from "../../interfaces";
interface HighspeedSegment {
startingSegmentNumber: number,
segmentLength: number,
highestSpeed: number,
highestSpeedSegmentNumber: number
}
function createAndAppendChild(document: XMLDocument, parent: HTMLElement, nameOfChild: string, contents?: string): HTMLElement {
let childElement = document.createElement(nameOfChild);
parent.appendChild(childElement);
if (contents) {
childElement.appendChild(document.createTextNode(contents));
}
return childElement;
}
function createRoutePoint(document: XMLDocument, parent: HTMLElement, coordinate: Coordinate, requiredSpeed: number) {
let routePoint = createAndAppendChild(document, parent, 'rtept');
routePoint.setAttribute('lat', coordinate.latitude.toFixed(6));
routePoint.setAttribute('lon', coordinate.longitude.toFixed(6));
createAndAppendChild(document, routePoint, 'desc', 'Required speed: ' + (requiredSpeed * 3.6).toFixed(1) + ' km/h');
}
function createWaypoint(document: XMLDocument, parent: HTMLElement, name: string, coordinate: Coordinate, color: string): void {
let waypoint = createAndAppendChild(document, parent, 'wpt');
waypoint.setAttribute('lat', coordinate.latitude.toFixed(6));
waypoint.setAttribute('lon', coordinate.longitude.toFixed(6));
createAndAppendChild(document, waypoint, 'name', name);
let extensions = createAndAppendChild(document, waypoint, 'extensions');
let colorTag = document.createElementNS('https://osmand.net/docs/technical/osmand-file-formats/osmand-gpx', 'color');
extensions.appendChild(colorTag);
colorTag.appendChild(document.createTextNode(color));
}
export function downloadGpxFile(path: PathSegment[]): void {
let gpxDocument = document.implementation.createDocument('http://www.topografix.com/GPX/1/1', 'gpx');
gpxDocument.documentElement.setAttribute('version', '1.1');
gpxDocument.documentElement.setAttribute('creator', 'https://freewheeling.martinserver.no');
let routeElement = createAndAppendChild(gpxDocument, gpxDocument.documentElement, 'rte');
createAndAppendChild(gpxDocument, routeElement, 'name', 'Freewheeling route');
createAndAppendChild(gpxDocument, routeElement, 'desc', 'Route to be biked without pedalling');
createAndAppendChild(gpxDocument, routeElement, 'src', 'https://freewheeling.martinserver.no');
// Add first route point
if (path.length > 0) {
createRoutePoint(gpxDocument, routeElement, path.at(0)!.start, path.at(0)!.requiredSpeed);
}
// Add remaining route points
path.forEach(segment => {
createRoutePoint(gpxDocument, routeElement, segment.end, segment.requiredSpeed);
});
// Add waypoints where you need to start accelerating, where you reach top speed, and where you can start rolling normally again
var minimumSpeed: number = Math.min(...path.map(segment => segment.requiredSpeed));
var highspeedSegments: HighspeedSegment[] = [];
var currentSegment: HighspeedSegment | undefined = undefined;
for (var i = 0; i < path.length; i++) {
var segment = path[i];
if (segment.requiredSpeed - minimumSpeed > 0.01) {
if (currentSegment == null) {
currentSegment = {
startingSegmentNumber: i,
segmentLength: 1,
highestSpeed: -1,
highestSpeedSegmentNumber: -1
}
} else {
currentSegment.segmentLength += 1;
}
} else {
if (currentSegment != null) {
highspeedSegments.push(currentSegment);
currentSegment = undefined;
}
}
}
if (currentSegment != null) {
highspeedSegments.push(currentSegment);
}
// Find the highest speed inside each high speed segment
highspeedSegments.forEach(highspeedSegment => {
var highestSpeed = 0;
var highestSpeedSegmentNumber = 0;
for (var i = highspeedSegment.startingSegmentNumber; i < highspeedSegment.startingSegmentNumber + highspeedSegment.segmentLength; i++) {
if (path[i].requiredSpeed > highestSpeed) {
highestSpeed = path[i].requiredSpeed;
highestSpeedSegmentNumber = i;
}
}
highspeedSegment.highestSpeed = highestSpeed * 3.6;
highspeedSegment.highestSpeedSegmentNumber = highestSpeedSegmentNumber;
});
// We probably want to drop the segments where we're not at least doubling the minimum speed, and we want them to be at least three or four long
highspeedSegments = highspeedSegments.filter(segment => segment.highestSpeed >= 2.0 * minimumSpeed && segment.segmentLength > 4);
// Add these segments to the gpx
highspeedSegments.forEach(highspeedSegment => {
let firstSegment = path[highspeedSegment.startingSegmentNumber];
let lastSegment = path[highspeedSegment.startingSegmentNumber + highspeedSegment.segmentLength - 1];
let fastestSegment = path[highspeedSegment.highestSpeedSegmentNumber];
createWaypoint(gpxDocument, gpxDocument.documentElement, 'Accelerate to ' + highspeedSegment.highestSpeed.toFixed(1) + ' km/h', firstSegment.start, '#00FF00');
createWaypoint(gpxDocument, gpxDocument.documentElement, 'Reach ' + highspeedSegment.highestSpeed.toFixed(1) + ' km/h', fastestSegment.start, '#FFFF00');
createWaypoint(gpxDocument, gpxDocument.documentElement, 'High speed ends', lastSegment.start, '#FF0000');
});
var element = document.createElement('a');
element.setAttribute('href', 'data:application/gpx+xml;base64,' + btoa('<?xml version="1.0" encoding="utf-8"?>' + new XMLSerializer().serializeToString(gpxDocument)));
element.setAttribute('download', 'freewheeling-route.gpx');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
+114
View File
@@ -0,0 +1,114 @@
import type { PathSegment } from "../../interfaces";
interface HighspeedSegment {
startingSegmentNumber: number,
segmentLength: number,
highestSpeed: number,
highestSpeedSegmentNumber: number
}
function createAndAppendChild(document: XMLDocument, parent: HTMLElement, nameOfChild: string, contents?: string): HTMLElement {
let childElement = document.createElement(nameOfChild);
parent.appendChild(childElement);
if (contents) {
childElement.appendChild(document.createTextNode(contents));
}
return childElement;
}
function createPlacemark(document: XMLDocument, parent: HTMLElement, style: string, path: PathSegment[], firstIndex: number, lastIndex: number, name: string) {
let placemark = createAndAppendChild(document, parent, 'Placemark');
createAndAppendChild(document, placemark, 'Name', name);
createAndAppendChild(document, placemark, 'styleUrl', style);
let lineString = createAndAppendChild(document, placemark, 'LineString');
var coordinatesString = '\n';
coordinatesString += path[firstIndex].start.longitude.toFixed(6) + ',' + path[firstIndex].start.latitude.toFixed(6) + ',0\n';
for (var i = firstIndex; i < lastIndex; i++) {
coordinatesString += path[i].end.longitude.toFixed(6) + ',' + path[i].end.latitude.toFixed(6) + ',0\n';
}
createAndAppendChild(document, lineString, 'coordinates', coordinatesString);
}
export function downloadKmlFile(path: PathSegment[]): void {
let kmlDocument = document.implementation.createDocument('http://www.opengis.net/kml/2.2', 'kml');
let kmlRoot = createAndAppendChild(kmlDocument, kmlDocument.documentElement, 'Document');
createAndAppendChild(kmlDocument, kmlRoot, 'Name', 'Freewheeling route');
let slowStyle = createAndAppendChild(kmlDocument, kmlRoot, 'Style');
slowStyle.setAttribute('id', 'lowSpeed');
let slowLine = createAndAppendChild(kmlDocument, slowStyle, 'LineStyle');
createAndAppendChild(kmlDocument, slowLine, 'color', 'ff231be5');
let fastStyle = createAndAppendChild(kmlDocument, kmlRoot, 'Style');
fastStyle.setAttribute('id', 'highSpeed');
let fastLine = createAndAppendChild(kmlDocument, fastStyle, 'LineStyle');
createAndAppendChild(kmlDocument, fastLine, 'color', 'ff3c8c3c');
// Add waypoints where you need to start accelerating, where you reach top speed, and where you can start rolling normally again
var minimumSpeed: number = Math.min(...path.map(segment => segment.requiredSpeed));
var highspeedSegments: HighspeedSegment[] = [];
var currentSegment: HighspeedSegment | undefined = undefined;
for (var i = 0; i < path.length; i++) {
var segment = path[i];
if (segment.requiredSpeed - minimumSpeed > 0.01) {
if (currentSegment == null) {
currentSegment = {
startingSegmentNumber: i,
segmentLength: 1,
highestSpeed: -1,
highestSpeedSegmentNumber: -1
}
} else {
currentSegment.segmentLength += 1;
}
} else {
if (currentSegment != null) {
highspeedSegments.push(currentSegment);
currentSegment = undefined;
}
}
}
if (currentSegment != null) {
highspeedSegments.push(currentSegment);
}
// Find the highest speed inside each high speed segment
highspeedSegments.forEach(highspeedSegment => {
var highestSpeed = 0;
var highestSpeedSegmentNumber = 0;
for (var i = highspeedSegment.startingSegmentNumber; i < highspeedSegment.startingSegmentNumber + highspeedSegment.segmentLength; i++) {
if (path[i].requiredSpeed > highestSpeed) {
highestSpeed = path[i].requiredSpeed;
highestSpeedSegmentNumber = i;
}
}
highspeedSegment.highestSpeed = highestSpeed * 3.6;
highspeedSegment.highestSpeedSegmentNumber = highestSpeedSegmentNumber;
});
// We probably want to drop the segments where we're not at least doubling the minimum speed, and we want them to be at least three or four long
highspeedSegments = highspeedSegments.filter(segment => segment.highestSpeed >= 2.0 * minimumSpeed && segment.segmentLength > 4);
var pointCounter = 0;
highspeedSegments.forEach(highspeedSegment => {
if (pointCounter < highspeedSegment.startingSegmentNumber) {
createPlacemark(kmlDocument, kmlRoot, '#lowSpeed', path, pointCounter, highspeedSegment.startingSegmentNumber, 'Freeroll');
}
let lastNumber = highspeedSegment.startingSegmentNumber + highspeedSegment.segmentLength;
createPlacemark(kmlDocument, kmlRoot, '#highSpeed', path, highspeedSegment.startingSegmentNumber, lastNumber, highspeedSegment.highestSpeed.toFixed(1) + ' km/h');
pointCounter = lastNumber;
});
if (pointCounter < path.length) {
createPlacemark(kmlDocument, kmlRoot, '#lowSpeed', path, pointCounter, path.length, 'Freeroll');
}
var element = document.createElement('a');
element.setAttribute('href', 'data:application/gpx+xml;base64,' + btoa('<?xml version="1.0" encoding="utf-8"?>' + new XMLSerializer().serializeToString(kmlDocument)));
element.setAttribute('download', 'freewheeling-route.kml');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
+22 -10
View File
@@ -4,7 +4,7 @@ import '@geoman-io/leaflet-geoman-free'
import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css' import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css'
import './maphandler.css' import './maphandler.css'
import { greenIcon, redIcon, violetIcon } from './icons.ts' import { greenIcon, redIcon } from './icons.ts'
import type { Endpoint, Polygon as InterfacePolygon, PathSegment } from '../../interfaces.ts' import type { Endpoint, Polygon as InterfacePolygon, PathSegment } from '../../interfaces.ts'
import type { Position } from 'geojson'; import type { Position } from 'geojson';
@@ -250,32 +250,44 @@ class MapHandler {
this.searchAreaPolygonListeners.push(searchAreaListener); this.searchAreaPolygonListeners.push(searchAreaListener);
} }
public drawStartNode(nodeId: number, latitude: number, longitude: number): void { public drawStartNode(nodeId: number, latitude: number, longitude: number, reverse: boolean): void {
this.path.clearLayers(); this.path.clearLayers();
if (this.startMarker != null) { if (this.startMarker != null) {
this.map.removeLayer(this.startMarker); this.map.removeLayer(this.startMarker);
} }
var settings;
if (reverse) {
settings = {icon: redIcon};
} else {
settings = {icon: greenIcon};
}
this.currentStartPoint = nodeId; this.currentStartPoint = nodeId;
this.endMarkers.clearLayers(); this.endMarkers.clearLayers();
this.startMarker = L.marker([latitude, longitude], {icon: greenIcon}).addTo(this.map); this.startMarker = L.marker([latitude, longitude], settings).addTo(this.map);
} }
public drawEndPoints(endpoints: Endpoint[]): void { public drawEndPoints(endpoints: Endpoint[], reverse: boolean): void {
this.endMarkers.clearLayers(); this.endMarkers.clearLayers();
this.path.clearLayers(); this.path.clearLayers();
var firstMarker = true; var firstMarker = true;
var color = reverse ? 'orange' : 'violet';
var settings: L.MarkerOptions;
if (reverse) {
settings = {icon: greenIcon};
} else {
settings = {icon: redIcon};
}
endpoints.forEach(endpoint => { endpoints.forEach(endpoint => {
var settings;
var marker; var marker;
if (firstMarker) { if (firstMarker) {
marker = L.marker([endpoint.latitude, endpoint.longitude], {icon: redIcon}).addTo(this.endMarkers); marker = L.marker([endpoint.latitude, endpoint.longitude], settings).addTo(this.endMarkers);
settings = {
icon: redIcon
};
} else { } else {
marker = L.circleMarker([endpoint.latitude, endpoint.longitude], {radius: 2, fillOpacity: 1.0, color: 'purple', bubblingMouseEvents: false}).addTo(this.endMarkers); marker = L.circleMarker([endpoint.latitude, endpoint.longitude], {radius: 2, fillOpacity: 1.0, color: color, bubblingMouseEvents: false}).addTo(this.endMarkers);
} }
marker.bindTooltip(Math.round(endpoint.distanceFromStart) + 'm'); marker.bindTooltip(Math.round(endpoint.distanceFromStart) + 'm');
+23 -16
View File
@@ -4,6 +4,7 @@ import proj4 from 'proj4';
import type { Endpoint, Message, PathSegment, Polygon, SearchAreaResultEntry } from '../../interfaces'; import type { Endpoint, Message, PathSegment, Polygon, SearchAreaResultEntry } from '../../interfaces';
var dataLoaded = false; var dataLoaded = false;
var fileEpsg: string;
var module: MainModule | undefined = undefined; var module: MainModule | undefined = undefined;
function sendErrorMessage(error: string): void { function sendErrorMessage(error: string): void {
@@ -19,9 +20,9 @@ function createCMultiPolygon(module: MainModule, polygons: Polygon[]): MultiPoly
let cRing = new module.Ring(); let cRing = new module.Ring();
ring.coordinates.forEach(coordinate => { ring.coordinates.forEach(coordinate => {
let polygonCoordinate = new module.PolygonCoordinate(); let polygonCoordinate = new module.PolygonCoordinate();
let utmCoordinates = proj4('EPSG:4326', 'EPSG:32633', [coordinate.longitude, coordinate.latitude]); let fileCoordinates = proj4('EPSG:4326', fileEpsg, [coordinate.longitude, coordinate.latitude]);
polygonCoordinate.x = utmCoordinates[0]; polygonCoordinate.x = fileCoordinates[0];
polygonCoordinate.y = utmCoordinates[1]; polygonCoordinate.y = fileCoordinates[1];
cRing.push_back(polygonCoordinate); cRing.push_back(polygonCoordinate);
}); });
cPolygon.push_back(cRing); cPolygon.push_back(cRing);
@@ -36,8 +37,8 @@ function getAreaSearchResults(searchedNodes: AreaSearchEntries): SearchAreaResul
for (var i = 0; i < searchedNodes.size(); i++) { for (var i = 0; i < searchedNodes.size(); i++) {
let searchedNode = searchedNodes.get(i); let searchedNode = searchedNodes.get(i);
if (searchedNode != null) { if (searchedNode != null) {
let utmCoordinates = [searchedNode.positionX, searchedNode.positionY]; let fileCoordinates = [searchedNode.positionX, searchedNode.positionY];
let lngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', utmCoordinates); let lngLatCoordinates = proj4(fileEpsg, 'EPSG:4326', fileCoordinates);
searchResults.push({ searchResults.push({
nodeId: searchedNode.nodeId, nodeId: searchedNode.nodeId,
latitude: lngLatCoordinates[1], latitude: lngLatCoordinates[1],
@@ -61,6 +62,8 @@ onmessage = async (e) => {
module.loadData('roads.dat'); module.loadData('roads.dat');
dataLoaded = true; dataLoaded = true;
fileEpsg = module.getEpsgCode();
let returnMessage: Message = {dataLoaded: {}} let returnMessage: Message = {dataLoaded: {}}
postMessage(returnMessage); postMessage(returnMessage);
return; return;
@@ -74,11 +77,11 @@ onmessage = async (e) => {
if (message.findClosestNode != null) { if (message.findClosestNode != null) {
let lngLatCoordinates = [message.findClosestNode.longitude, message.findClosestNode.latitude]; let lngLatCoordinates = [message.findClosestNode.longitude, message.findClosestNode.latitude];
let utmCoordinates = proj4('EPSG:4326', 'EPSG:32633', lngLatCoordinates); let fileCoordinates = proj4('EPSG:4326', fileEpsg, lngLatCoordinates);
let node = module.findClosestNode(utmCoordinates[0], utmCoordinates[1]); let node = module.findClosestNode(fileCoordinates[0], fileCoordinates[1]);
let nodeUtmCoordinates = [node.positionX, node.positionY]; let nodeFileCoordinates = [node.positionX, node.positionY];
let nodeLngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', nodeUtmCoordinates); let nodeLngLatCoordinates = proj4(fileEpsg, 'EPSG:4326', nodeFileCoordinates);
let returnMessage: Message = { let returnMessage: Message = {
foundClosestNode: { foundClosestNode: {
@@ -102,7 +105,9 @@ onmessage = async (e) => {
message.findPathsFromNode.dragCoefficient, message.findPathsFromNode.dragCoefficient,
message.findPathsFromNode.allowMotorways, message.findPathsFromNode.allowMotorways,
message.findPathsFromNode.allowTunnels, message.findPathsFromNode.allowTunnels,
message.findPathsFromNode.allowAgainstOneway message.findPathsFromNode.allowAgainstOneway,
message.findPathsFromNode.limitCornerSpeed,
message.findPathsFromNode.reverse
); );
let endpoints: Endpoint[] = []; let endpoints: Endpoint[] = [];
@@ -113,7 +118,7 @@ onmessage = async (e) => {
return; return;
} }
let coordinates = [nodeData.positionX, nodeData.positionY]; let coordinates = [nodeData.positionX, nodeData.positionY];
let lngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', coordinates); let lngLatCoordinates = proj4(fileEpsg, 'EPSG:4326', coordinates);
endpoints.push({ endpoints.push({
nodeId: nodeData.nodeId, nodeId: nodeData.nodeId,
latitude: lngLatCoordinates[1], latitude: lngLatCoordinates[1],
@@ -121,9 +126,10 @@ onmessage = async (e) => {
distanceFromStart: nodeData.distanceFromStart distanceFromStart: nodeData.distanceFromStart
}); });
} }
let reverse: boolean = results.reverse;
results.delete(); results.delete();
let returnMessage: Message = {foundPathsFromNode: {nodeId: message.findPathsFromNode.nodeId, endpoints: endpoints}}; let returnMessage: Message = {foundPathsFromNode: {nodeId: message.findPathsFromNode.nodeId, endpoints: endpoints, reverse: reverse}};
postMessage(returnMessage); postMessage(returnMessage);
} }
@@ -143,11 +149,11 @@ onmessage = async (e) => {
if (!previousPoint) { if (!previousPoint) {
continue; continue;
} }
let previousUtm = [previousPoint.positionX, previousPoint.positionY]; let previousFileCoordinates = [previousPoint.positionX, previousPoint.positionY];
let currentUtm = [currentPoint.positionX, currentPoint.positionY]; let currentFileCoordinates = [currentPoint.positionX, currentPoint.positionY];
let previousLngLat = proj4('EPSG:32633', 'EPSG:4326', previousUtm); let previousLngLat = proj4(fileEpsg, 'EPSG:4326', previousFileCoordinates);
let currentLngLat = proj4('EPSG:32633', 'EPSG:4326', currentUtm); let currentLngLat = proj4(fileEpsg, 'EPSG:4326', currentFileCoordinates);
let speed = Math.max(previousPoint.currentSpeed, currentPoint.currentSpeed); let speed = Math.max(previousPoint.currentSpeed, currentPoint.currentSpeed);
let requiredSpeed = Math.max(previousPoint.requiredSpeed, currentPoint.requiredSpeed); let requiredSpeed = Math.max(previousPoint.requiredSpeed, currentPoint.requiredSpeed);
@@ -186,6 +192,7 @@ onmessage = async (e) => {
settings.allowMotorways, settings.allowMotorways,
settings.allowTunnels, settings.allowTunnels,
settings.allowAgainstOneway, settings.allowAgainstOneway,
settings.limitCornerSpeed,
createCMultiPolygon(module, settings.polygons) createCMultiPolygon(module, settings.polygons)
); );
+30 -3
View File
@@ -35,7 +35,7 @@ p {
} }
#settings-div { #settings-div {
width: 40vw; width: max(40vw, 800px);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
border-style: solid; border-style: solid;
@@ -56,7 +56,7 @@ p {
} }
table { table {
width: max(60vw, 800px); width: min(92vw, 800px);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
border: solid 1px; border: solid 1px;
@@ -66,9 +66,36 @@ table {
th { th {
border: solid 1px; border: solid 1px;
width: max(15vw, 200px); width: min(23vw, 200px);
} }
td { td {
border: solid 1px; border: solid 1px;
}
@media only screen and (max-width: 800px) {
.hidable {
display: none;
}
h1 {
font-size: 60px;
}
table {
width: min(90vw, 400px);
}
th {
width: min(45vw, 200px);
}
#settings-div {
width: 80vw;
}
.settings-line {
display: flex;
flex-direction: column;
}
} }