Compare commits
9
Commits
b3b3a5b652
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c914996a2 | ||
|
|
32ed39c61a | ||
|
|
5bfd56d56e | ||
|
|
dc2e8170a4 | ||
|
|
2ca8a14d8f | ||
|
|
46aacb2310 | ||
|
|
fb0247841b | ||
|
|
c61a76a6e4 | ||
|
|
c838fb12b1 |
@@ -1,5 +1,12 @@
|
|||||||
|
.gitattributes
|
||||||
.vscode/
|
.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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+10
-1
@@ -31,7 +31,10 @@
|
|||||||
<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>
|
||||||
@@ -80,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>.
|
||||||
|
|||||||
+300
-215
@@ -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,23 +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;
|
||||||
float course;
|
float course;
|
||||||
uint8_t speed_limit;
|
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 {
|
||||||
@@ -46,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 {
|
||||||
@@ -60,12 +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 current_course;
|
float currentCourse;
|
||||||
|
|
||||||
ListNode* next = NULL;
|
ListNode* next = NULL;
|
||||||
};
|
};
|
||||||
@@ -104,6 +112,8 @@ struct CurrentAreaSearch {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -117,34 +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(to_x - from_x, to_y - from_y) * 180 / 3.14152965;
|
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.course = course;
|
connection.course = course;
|
||||||
connection.speed_limit = speed_limit;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,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;
|
||||||
@@ -229,107 +258,143 @@ 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
|
|
||||||
float slope = atan(slope_tan);
|
|
||||||
float slope_sin = sin(slope);
|
|
||||||
float full_distance = horizontal_distance * slope_tan / slope_sin;
|
|
||||||
float acceleration = -GRAVITY_ACCELERATION * slope_sin;
|
|
||||||
float terminal_velocity = sqrt(fabs(acceleration) / drag_coefficient);
|
|
||||||
|
|
||||||
// Uphill
|
// If the slope is not flat, we should calculate some trig identities, and how long the slope is
|
||||||
if (slope > 0) {
|
float slope = atan(slopeTan);
|
||||||
float time_to_peak = atan(starting_speed / terminal_velocity) / (drag_coefficient * terminal_velocity);
|
float slopeSin = sin(slope);
|
||||||
// If the discriminant is greater than 1, the slope is so steep that we cannot reach the end with our starting speed
|
float fullDistance = horizontalDistance * slopeTan / slopeSin;
|
||||||
float discriminant = cos(drag_coefficient * terminal_velocity * time_to_peak) * exp(full_distance * drag_coefficient);
|
|
||||||
if (discriminant > 1.f) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
float time_to_reach_end = time_to_peak - acos(discriminant) / (drag_coefficient * terminal_velocity);
|
// We need to calculate the terminal velocity given the slope we're in
|
||||||
final_speed = terminal_velocity * tan(drag_coefficient * terminal_velocity * (time_to_peak - time_to_reach_end));
|
float terminalVelocity = sqrt(fabs(GRAVITY_ACCELERATION * slopeSin) / dragCoefficient);
|
||||||
} else {
|
|
||||||
// Downhill
|
// First, calculate the final speed if we're going uphill
|
||||||
// If the starting speed is very close to the terminal velocity, we'll just stay at terminal velocity
|
if (slope > 0) {
|
||||||
if (fabs(starting_speed - terminal_velocity) < 0.001) {
|
float timeToPeak = atan(startingSpeed / terminalVelocity) / (dragCoefficient * terminalVelocity);
|
||||||
final_speed = terminal_velocity;
|
float discriminant = exp(fullDistance * dragCoefficient) * cos(dragCoefficient * terminalVelocity * timeToPeak);
|
||||||
} else if (starting_speed < terminal_velocity) {
|
if (discriminant > 1.f) {
|
||||||
float k1 = terminal_velocity * log((terminal_velocity + starting_speed) / (terminal_velocity - starting_speed)) * 0.5;
|
// If this value is greater than 1, it means that the slope is too steep and we can never reach the top with our
|
||||||
float k2 = -log(cosh(k1 / terminal_velocity)) / drag_coefficient;
|
// starting speed
|
||||||
float time_spent = acosh(exp(drag_coefficient * (full_distance - k2))) / (drag_coefficient * terminal_velocity) - k1 / (drag_coefficient * pow(terminal_velocity, 2));
|
return -1;
|
||||||
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, bool limitCornerSpeed) {
|
SearchResult findAllPathsFromPoint(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, bool reverse) {
|
||||||
SearchResult result;
|
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;
|
||||||
|
|
||||||
nextNode->id = startingNode;
|
nextNode->id = startingNode;
|
||||||
nextNode->current_speed = minimum_speed;
|
nextNode->currentSpeed = minimumSpeed;
|
||||||
nextNode->current_course = 0;
|
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->current_course;
|
float currentCourse = currentNode->currentCourse;
|
||||||
|
|
||||||
RoadNode bestNode = set.roadNodes[currentId];
|
RoadNode bestNode = set.roadNodes[currentId];
|
||||||
delete currentNode;
|
delete currentNode;
|
||||||
@@ -341,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,18 +422,35 @@ 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 we limit the speed on corners, do that here
|
||||||
if (limitCornerSpeed) {
|
if (limitCornerSpeed) {
|
||||||
float courseDifference = fabs(currentCourse - neighbour.course);
|
float courseDifference = fabs(currentCourse - neighbour.course);
|
||||||
@@ -376,46 +458,80 @@ SearchResult findAllPathsFromPoint(int startingNode, float minimum_speed, float
|
|||||||
courseDifference = 360 - courseDifference;
|
courseDifference = 360 - courseDifference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float maximumCornerSpeed;
|
||||||
if (courseDifference > 95) {
|
if (courseDifference > 95) {
|
||||||
resultingSpeed = minimum_speed;
|
maximumCornerSpeed = minimumSpeed;
|
||||||
} else if (courseDifference > 45.0) {
|
} else if (courseDifference > 45.0) {
|
||||||
float maximumCornerSpeed = (95 - courseDifference) / 50.0 * (maximum_speed - minimum_speed) + minimum_speed;
|
maximumCornerSpeed = (95 - courseDifference) / 50.0 * (maximumSpeed - minimumSpeed) + minimumSpeed;
|
||||||
resultingSpeed = fmin(resultingSpeed, maximumCornerSpeed);
|
} 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->current_course = neighbour.course;
|
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;
|
||||||
} else {
|
nextNode = neighbourListNode;
|
||||||
ListNode* previousSearchNode = nextNode;
|
} else {
|
||||||
ListNode* currentSearchNode = nextNode->next;
|
ListNode* previousSearchNode = nextNode;
|
||||||
|
ListNode* currentSearchNode = nextNode->next;
|
||||||
|
|
||||||
while(currentSearchNode != NULL && currentSearchNode->current_speed > resultingSpeed) {
|
while (currentSearchNode != NULL && currentSearchNode->currentSpeed < resultingSpeed) {
|
||||||
previousSearchNode = currentSearchNode;
|
previousSearchNode = currentSearchNode;
|
||||||
currentSearchNode = currentSearchNode->next;
|
currentSearchNode = currentSearchNode->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousSearchNode->next = neighbourListNode;
|
||||||
|
neighbourListNode->next = currentSearchNode;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (nextNode == NULL || resultingSpeed < nextNode->currentSpeed) {
|
||||||
|
neighbourListNode->next = nextNode;
|
||||||
|
nextNode = neighbourListNode;
|
||||||
|
} else {
|
||||||
|
ListNode* previousSearchNode = nextNode;
|
||||||
|
ListNode* currentSearchNode = nextNode->next;
|
||||||
|
|
||||||
previousSearchNode->next = neighbourListNode;
|
while(currentSearchNode != NULL && currentSearchNode->currentSpeed > resultingSpeed) {
|
||||||
neighbourListNode->next = currentSearchNode;
|
previousSearchNode = currentSearchNode;
|
||||||
|
currentSearchNode = currentSearchNode->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousSearchNode->next = neighbourListNode;
|
||||||
|
neighbourListNode->next = currentSearchNode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,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, bool limitCornerSpeed) {
|
JSSearchResult findAllPathsFromPointJS(int startingNode, float minimumSpeed, float maximumSpeed, int maximumSpeedLimit, float dragCoefficient, bool allowMotorways, bool allowTunnels, bool allowAgainstOneway, bool limitCornerSpeed, bool reverse) {
|
||||||
lastSearchResult = findAllPathsFromPoint(startingNode, minimumSpeed, maximumSpeed, maximumSpeedLimit, dragCoefficient, allowMotorways, allowTunnels, allowAgainstOneway, limitCornerSpeed);
|
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;
|
||||||
@@ -440,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);
|
||||||
}
|
}
|
||||||
@@ -475,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;
|
||||||
|
|
||||||
@@ -544,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;
|
||||||
@@ -556,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) {
|
||||||
@@ -567,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;
|
||||||
@@ -652,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -699,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) {
|
||||||
@@ -708,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;
|
||||||
@@ -729,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
|
||||||
);
|
);
|
||||||
@@ -767,7 +847,8 @@ AreaSearchResult continueAreaSearch() {
|
|||||||
currentAreaSearch.allowMotorways,
|
currentAreaSearch.allowMotorways,
|
||||||
currentAreaSearch.allowTunnels,
|
currentAreaSearch.allowTunnels,
|
||||||
currentAreaSearch.allowAgainstOneway,
|
currentAreaSearch.allowAgainstOneway,
|
||||||
currentAreaSearch.limitCornerSpeed
|
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
|
||||||
@@ -793,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;
|
||||||
@@ -869,8 +950,8 @@ AreaSearchResult continueAreaSearch() {
|
|||||||
if (overlapCounts.size() == 0) {
|
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(
|
||||||
@@ -901,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<>()
|
||||||
@@ -914,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<>()
|
||||||
@@ -940,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);
|
||||||
@@ -947,3 +1031,4 @@ EMSCRIPTEN_BINDINGS(my_module) {
|
|||||||
emscripten::function("startAreaSearch", &startAreaSearch);
|
emscripten::function("startAreaSearch", &startAreaSearch);
|
||||||
emscripten::function("continueAreaSearch", &continueAreaSearch);
|
emscripten::function("continueAreaSearch", &continueAreaSearch);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
+4
-2
@@ -45,7 +45,8 @@ interface FindPathsFromNode {
|
|||||||
allowMotorways: boolean,
|
allowMotorways: boolean,
|
||||||
allowTunnels: boolean,
|
allowTunnels: boolean,
|
||||||
allowAgainstOneway: boolean,
|
allowAgainstOneway: boolean,
|
||||||
limitCornerSpeed: boolean
|
limitCornerSpeed: boolean,
|
||||||
|
reverse: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Endpoint {
|
export interface Endpoint {
|
||||||
@@ -57,7 +58,8 @@ export interface Endpoint {
|
|||||||
|
|
||||||
interface FoundPathsFromNode {
|
interface FoundPathsFromNode {
|
||||||
nodeId: number,
|
nodeId: number,
|
||||||
endpoints: Endpoint[]
|
endpoints: Endpoint[],
|
||||||
|
reverse: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetFullPath {
|
interface GetFullPath {
|
||||||
|
|||||||
+16
-11
@@ -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'
|
||||||
@@ -17,7 +17,8 @@ interface Settings {
|
|||||||
allowTunnels: boolean,
|
allowTunnels: boolean,
|
||||||
allowAgainstOneway: boolean,
|
allowAgainstOneway: boolean,
|
||||||
limitCornerSpeed: boolean,
|
limitCornerSpeed: boolean,
|
||||||
cutoffDistance: number
|
cutoffDistance: number,
|
||||||
|
reverse: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default settings values
|
// Default settings values
|
||||||
@@ -47,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');
|
||||||
@@ -72,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,
|
||||||
@@ -83,11 +85,12 @@ routeWorker.onmessage = e => {
|
|||||||
allowMotorways: settings.allowMotorways,
|
allowMotorways: settings.allowMotorways,
|
||||||
allowTunnels: settings.allowTunnels,
|
allowTunnels: settings.allowTunnels,
|
||||||
allowAgainstOneway: settings.allowAgainstOneway,
|
allowAgainstOneway: settings.allowAgainstOneway,
|
||||||
limitCornerSpeed: settings.limitCornerSpeed
|
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);
|
||||||
@@ -130,8 +133,7 @@ routeWorker.onmessage = e => {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -142,7 +144,8 @@ routeWorker.onmessage = e => {
|
|||||||
allowMotorways: settings.allowMotorways,
|
allowMotorways: settings.allowMotorways,
|
||||||
allowTunnels: settings.allowTunnels,
|
allowTunnels: settings.allowTunnels,
|
||||||
allowAgainstOneway: settings.allowAgainstOneway,
|
allowAgainstOneway: settings.allowAgainstOneway,
|
||||||
limitCornerSpeed: settings.limitCornerSpeed
|
limitCornerSpeed: settings.limitCornerSpeed,
|
||||||
|
reverse: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
routeWorker.postMessage(requestMessage);
|
routeWorker.postMessage(requestMessage);
|
||||||
@@ -195,7 +198,8 @@ function setUpMapHandler() {
|
|||||||
allowMotorways: settings.allowMotorways,
|
allowMotorways: settings.allowMotorways,
|
||||||
allowTunnels: settings.allowTunnels,
|
allowTunnels: settings.allowTunnels,
|
||||||
allowAgainstOneway: settings.allowAgainstOneway,
|
allowAgainstOneway: settings.allowAgainstOneway,
|
||||||
limitCornerSpeed: settings.limitCornerSpeed
|
limitCornerSpeed: settings.limitCornerSpeed,
|
||||||
|
reverse: settings.reverse
|
||||||
}};
|
}};
|
||||||
routeWorker.postMessage(newRoutesMessage);
|
routeWorker.postMessage(newRoutesMessage);
|
||||||
}
|
}
|
||||||
@@ -325,7 +329,8 @@ function getSettings(): Settings {
|
|||||||
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),
|
||||||
limitCornerSpeed: getBooleanValue(limitCornerSpeedInput, DEFAULT_LIMIT_CORNER_SPEED),
|
limitCornerSpeed: getBooleanValue(limitCornerSpeedInput, DEFAULT_LIMIT_CORNER_SPEED),
|
||||||
cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE)
|
cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE),
|
||||||
|
reverse: getBooleanValue(reverseCheckbox, false)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +383,7 @@ searchButton?.addEventListener('click', _ => {
|
|||||||
|
|
||||||
exportButton?.addEventListener('click', _ => {
|
exportButton?.addEventListener('click', _ => {
|
||||||
if (lastRoute != null) {
|
if (lastRoute != null) {
|
||||||
downloadGpxFile(lastRoute);
|
downloadKmlFile(lastRoute);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -250,28 +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 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);
|
||||||
} 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');
|
||||||
|
|||||||
@@ -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: {
|
||||||
@@ -103,7 +106,8 @@ onmessage = async (e) => {
|
|||||||
message.findPathsFromNode.allowMotorways,
|
message.findPathsFromNode.allowMotorways,
|
||||||
message.findPathsFromNode.allowTunnels,
|
message.findPathsFromNode.allowTunnels,
|
||||||
message.findPathsFromNode.allowAgainstOneway,
|
message.findPathsFromNode.allowAgainstOneway,
|
||||||
message.findPathsFromNode.limitCornerSpeed
|
message.findPathsFromNode.limitCornerSpeed,
|
||||||
|
message.findPathsFromNode.reverse
|
||||||
);
|
);
|
||||||
|
|
||||||
let endpoints: Endpoint[] = [];
|
let endpoints: Endpoint[] = [];
|
||||||
@@ -114,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],
|
||||||
@@ -122,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,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);
|
||||||
|
|||||||
+11
-3
@@ -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,7 +66,7 @@ table {
|
|||||||
|
|
||||||
th {
|
th {
|
||||||
border: solid 1px;
|
border: solid 1px;
|
||||||
width: max(15vw, 200px);
|
width: min(23vw, 200px);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
@@ -78,8 +78,16 @@ td {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: max(30vw, 400px);
|
width: min(90vw, 400px);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
width: min(45vw, 200px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#settings-div {
|
#settings-div {
|
||||||
|
|||||||
Reference in New Issue
Block a user