mamut-routing-tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mamut_routing_tools-0.1.0/.gitignore +13 -0
- mamut_routing_tools-0.1.0/CITATION.cff +51 -0
- mamut_routing_tools-0.1.0/LICENSE +21 -0
- mamut_routing_tools-0.1.0/PKG-INFO +69 -0
- mamut_routing_tools-0.1.0/README.md +38 -0
- mamut_routing_tools-0.1.0/pyproject.toml +79 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/__init__.py +4 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/cli.py +103 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/geo.py +110 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/geometry/__init__.py +3 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/geometry/materialize.py +213 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/osm/__init__.py +3 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/osm/fetch.py +344 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/roadgraph/__init__.py +17 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/roadgraph/build.py +396 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/roadgraph/osmxml.py +210 -0
- mamut_routing_tools-0.1.0/src/mamut_routing_tools/roadgraph/router.py +43 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# This CITATION.cff file was generated with cffinit.
|
|
2
|
+
# Visit https://bit.ly/cffinit to generate yours today!
|
|
3
|
+
|
|
4
|
+
cff-version: 1.2.0
|
|
5
|
+
title: MAMUT-routing-tools
|
|
6
|
+
message: >-
|
|
7
|
+
If you use MAMUT-routing-tools, please cite it using the
|
|
8
|
+
metadata from this file.
|
|
9
|
+
type: software
|
|
10
|
+
authors:
|
|
11
|
+
- given-names: Florian
|
|
12
|
+
family-names: Rascoussier
|
|
13
|
+
alias: Onyr
|
|
14
|
+
affiliation: IMT Atlantique; INSA Lyon
|
|
15
|
+
orcid: "https://orcid.org/0009-0005-3253-9814"
|
|
16
|
+
- given-names: Adrien
|
|
17
|
+
family-names: Pichon
|
|
18
|
+
alias: Anzury
|
|
19
|
+
affiliation: Université Bretagne Sud
|
|
20
|
+
orcid: "https://orcid.org/0009-0005-8630-3962"
|
|
21
|
+
repository-code: "https://github.com/ANR-MAMUT/MAMUT-routing-tools"
|
|
22
|
+
abstract: >-
|
|
23
|
+
MAMUT-routing-tools is the local generation tool suite of the
|
|
24
|
+
MAMUT-routing benchmark project: OpenStreetMap city acquisition,
|
|
25
|
+
a road-graph engine compatible with the project's published
|
|
26
|
+
OSM-derived benchmark data, best-known-solution route-geometry
|
|
27
|
+
materialization, and interactive CVRP/VRPTW instance generation.
|
|
28
|
+
It complements the MAMUT-routing benchmark repository and its
|
|
29
|
+
static website by running all compute-heavy workflows locally,
|
|
30
|
+
supporting reproducible computational experiments in Operations
|
|
31
|
+
Research, Computer Science, and Applied Algorithmics.
|
|
32
|
+
keywords:
|
|
33
|
+
- benchmarking
|
|
34
|
+
- CVRP
|
|
35
|
+
- VRPTW
|
|
36
|
+
- reproducibility
|
|
37
|
+
- vehicle routing problem
|
|
38
|
+
- instance generation
|
|
39
|
+
- best-known solutions
|
|
40
|
+
- BKS
|
|
41
|
+
- open science
|
|
42
|
+
- optimization benchmarks
|
|
43
|
+
- OpenStreetMap
|
|
44
|
+
- OSM
|
|
45
|
+
- road network
|
|
46
|
+
- urban logistics
|
|
47
|
+
- research software
|
|
48
|
+
- routing
|
|
49
|
+
license: MIT
|
|
50
|
+
version: 0.1.0
|
|
51
|
+
date-released: "2026-07-18"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ANR-MAMUT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mamut-routing-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local generation tool suite for MAMUT-routing: OSM acquisition, road-graph engine, route geometry, and CVRP/VRPTW instance generation.
|
|
5
|
+
Project-URL: Repository, https://github.com/ANR-MAMUT/MAMUT-routing-tools
|
|
6
|
+
Project-URL: Website, https://mamut-routing.univ-ubs.fr/
|
|
7
|
+
Author: Adrien Pichon
|
|
8
|
+
Author-email: Florian Rascoussier <onyr.maintainer@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: CVRP,OSM,OpenStreetMap,VRP,VRPTW,benchmarking,instance generation,operations research,road network,vehicle routing problem
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: mamut-routing-lib
|
|
25
|
+
Requires-Dist: numpy>=1.26
|
|
26
|
+
Requires-Dist: pydantic>=2.7
|
|
27
|
+
Requires-Dist: rustworkx>=0.15
|
|
28
|
+
Requires-Dist: scipy>=1.13
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# MAMUT-routing-tools
|
|
33
|
+
|
|
34
|
+
Local generation tool suite for the [MAMUT-routing](https://github.com/ANR-MAMUT/MAMUT-routing) benchmark project: OSM city acquisition, a road-graph engine, BKS route-geometry materialization, and interactive CVRP/VRPTW instance generation. The public MAMUT-routing website is fully static; everything compute-heavy lives here and runs on your own machine.
|
|
35
|
+
|
|
36
|
+
Part of the [ANR MAMUT project](https://mamut.roadef.org/).
|
|
37
|
+
|
|
38
|
+
## Status
|
|
39
|
+
|
|
40
|
+
Beta. The tool suite is being extracted from the website's former Julia backend; interfaces may change between releases.
|
|
41
|
+
|
|
42
|
+
## Components
|
|
43
|
+
|
|
44
|
+
- `mamut-tools roadgraph`: build and inspect drivable road graphs from OSM XML extracts. The construction is a faithful Python port of the OpenStreetMapX.jl pipeline the project previously used (same road classes, oneway rules, intersection segmentation, ENU distances, and strongly-connected trim), so graphs and route geometry stay consistent with previously published data.
|
|
45
|
+
- `mamut-tools geometry`: materialize road-following polylines for best-known solutions, in the exact artifact format the MAMUT-routing website consumes.
|
|
46
|
+
- Planned: OSM city fetch (Nominatim + Overpass), interactive CVRP/VRPTW generation with a local workbench GUI, and the official time-dependent benchmark campaign pipeline.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Requires Python >= 3.11 and [uv](https://github.com/astral-sh/uv).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone --recurse-submodules https://github.com/ANR-MAMUT/MAMUT-routing-tools.git
|
|
54
|
+
cd MAMUT-routing-tools
|
|
55
|
+
uv sync
|
|
56
|
+
uv run mamut-tools --help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The `MAMUT-routing-lib` contract library is vendored as a git submodule and installed as an editable dependency.
|
|
60
|
+
|
|
61
|
+
## Quick examples
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Road-graph statistics for a city extract
|
|
65
|
+
uv run mamut-tools roadgraph info path/to/City.osm
|
|
66
|
+
|
|
67
|
+
# Materialize a route-geometry group plan (website build contract)
|
|
68
|
+
uv run mamut-tools geometry materialize-plan plan.json --repo-root path/to/MAMUT-routing --result-dir out/
|
|
69
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# MAMUT-routing-tools
|
|
2
|
+
|
|
3
|
+
Local generation tool suite for the [MAMUT-routing](https://github.com/ANR-MAMUT/MAMUT-routing) benchmark project: OSM city acquisition, a road-graph engine, BKS route-geometry materialization, and interactive CVRP/VRPTW instance generation. The public MAMUT-routing website is fully static; everything compute-heavy lives here and runs on your own machine.
|
|
4
|
+
|
|
5
|
+
Part of the [ANR MAMUT project](https://mamut.roadef.org/).
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Beta. The tool suite is being extracted from the website's former Julia backend; interfaces may change between releases.
|
|
10
|
+
|
|
11
|
+
## Components
|
|
12
|
+
|
|
13
|
+
- `mamut-tools roadgraph`: build and inspect drivable road graphs from OSM XML extracts. The construction is a faithful Python port of the OpenStreetMapX.jl pipeline the project previously used (same road classes, oneway rules, intersection segmentation, ENU distances, and strongly-connected trim), so graphs and route geometry stay consistent with previously published data.
|
|
14
|
+
- `mamut-tools geometry`: materialize road-following polylines for best-known solutions, in the exact artifact format the MAMUT-routing website consumes.
|
|
15
|
+
- Planned: OSM city fetch (Nominatim + Overpass), interactive CVRP/VRPTW generation with a local workbench GUI, and the official time-dependent benchmark campaign pipeline.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Requires Python >= 3.11 and [uv](https://github.com/astral-sh/uv).
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone --recurse-submodules https://github.com/ANR-MAMUT/MAMUT-routing-tools.git
|
|
23
|
+
cd MAMUT-routing-tools
|
|
24
|
+
uv sync
|
|
25
|
+
uv run mamut-tools --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `MAMUT-routing-lib` contract library is vendored as a git submodule and installed as an editable dependency.
|
|
29
|
+
|
|
30
|
+
## Quick examples
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Road-graph statistics for a city extract
|
|
34
|
+
uv run mamut-tools roadgraph info path/to/City.osm
|
|
35
|
+
|
|
36
|
+
# Materialize a route-geometry group plan (website build contract)
|
|
37
|
+
uv run mamut-tools geometry materialize-plan plan.json --repo-root path/to/MAMUT-routing --result-dir out/
|
|
38
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mamut-routing-tools"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local generation tool suite for MAMUT-routing: OSM acquisition, road-graph engine, route geometry, and CVRP/VRPTW instance generation."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Florian Rascoussier", email = "onyr.maintainer@gmail.com" },
|
|
8
|
+
{ name = "Adrien Pichon" },
|
|
9
|
+
]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
keywords = [
|
|
14
|
+
"vehicle routing problem",
|
|
15
|
+
"VRP",
|
|
16
|
+
"CVRP",
|
|
17
|
+
"VRPTW",
|
|
18
|
+
"benchmarking",
|
|
19
|
+
"instance generation",
|
|
20
|
+
"OpenStreetMap",
|
|
21
|
+
"OSM",
|
|
22
|
+
"road network",
|
|
23
|
+
"operations research",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Scientific/Engineering",
|
|
36
|
+
]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"mamut-routing-lib",
|
|
39
|
+
"typer>=0.12",
|
|
40
|
+
"pydantic>=2.7",
|
|
41
|
+
"numpy>=1.26",
|
|
42
|
+
"scipy>=1.13",
|
|
43
|
+
"rustworkx>=0.15",
|
|
44
|
+
"httpx>=0.27",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Repository = "https://github.com/ANR-MAMUT/MAMUT-routing-tools"
|
|
49
|
+
Website = "https://mamut-routing.univ-ubs.fr/"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
mamut-tools = "mamut_routing_tools.cli:app"
|
|
53
|
+
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
dev = [
|
|
56
|
+
"pytest>=8.0",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[build-system]
|
|
60
|
+
requires = ["hatchling"]
|
|
61
|
+
build-backend = "hatchling.build"
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/mamut_routing_tools"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
exclude = [
|
|
68
|
+
".git",
|
|
69
|
+
".gitignore",
|
|
70
|
+
".gitmodules",
|
|
71
|
+
".github",
|
|
72
|
+
"MAMUT-routing-lib",
|
|
73
|
+
"nix-dev",
|
|
74
|
+
"tests",
|
|
75
|
+
"uv.lock",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""mamut-tools: command-line interface of MAMUT-routing-tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
name="mamut-tools",
|
|
13
|
+
help="Local MAMUT-routing tool suite: OSM acquisition, road-graph engine, route geometry, and instance generation.",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
add_completion=False,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
roadgraph_app = typer.Typer(help="Road-graph engine (OpenStreetMapX-compatible construction).", no_args_is_help=True)
|
|
19
|
+
geometry_app = typer.Typer(help="BKS route-geometry materialization.", no_args_is_help=True)
|
|
20
|
+
osm_app = typer.Typer(help="OSM city acquisition (Nominatim + Overpass).", no_args_is_help=True)
|
|
21
|
+
app.add_typer(roadgraph_app, name="roadgraph")
|
|
22
|
+
app.add_typer(geometry_app, name="geometry")
|
|
23
|
+
app.add_typer(osm_app, name="osm")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@osm_app.command("fetch-city")
|
|
27
|
+
def osm_fetch_city_cmd(
|
|
28
|
+
city: Annotated[str, typer.Argument(help="City or locality name to geocode and download.")],
|
|
29
|
+
country: Annotated[str, typer.Option("--country", help="Optional country to disambiguate the geocode.")] = "",
|
|
30
|
+
osm_dir: Annotated[Path, typer.Option("--osm-dir", help="Directory for the downloaded <city>.osm extract.")] = Path("osmdata"),
|
|
31
|
+
padding_km: Annotated[float, typer.Option("--padding-km", help="Extra bbox padding in km.")] = 0.0,
|
|
32
|
+
max_radius_km: Annotated[float, typer.Option("--max-radius-km", help="Clamp the administrative bbox to a square of this radius around the place's geocode point (0 = no clamp).")] = 0.0,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Download an OSM extract (roads + amenities) for a city by name."""
|
|
35
|
+
from mamut_routing_tools.osm import fetch_and_store_city_osm
|
|
36
|
+
|
|
37
|
+
summary = fetch_and_store_city_osm(
|
|
38
|
+
city,
|
|
39
|
+
country=country,
|
|
40
|
+
osm_dir=osm_dir,
|
|
41
|
+
padding_km=padding_km,
|
|
42
|
+
max_radius_km=max_radius_km,
|
|
43
|
+
)
|
|
44
|
+
typer.echo(json.dumps(summary, indent=1))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@roadgraph_app.command("info")
|
|
48
|
+
def roadgraph_info_cmd(
|
|
49
|
+
osm_path: Annotated[Path, typer.Argument(help="OSM XML extract to build the road graph from.")],
|
|
50
|
+
only_intersections: Annotated[bool, typer.Option("--only-intersections/--all-nodes")] = True,
|
|
51
|
+
trim_to_connected: Annotated[bool, typer.Option("--trim/--no-trim")] = True,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Build the road graph and print vertex/edge statistics."""
|
|
54
|
+
from mamut_routing_tools.roadgraph import load_road_graph
|
|
55
|
+
|
|
56
|
+
graph = load_road_graph(
|
|
57
|
+
osm_path,
|
|
58
|
+
only_intersections=only_intersections,
|
|
59
|
+
trim_to_connected=trim_to_connected,
|
|
60
|
+
)
|
|
61
|
+
class_counts: dict[int, int] = {}
|
|
62
|
+
for cls in graph.edge_class:
|
|
63
|
+
class_counts[cls] = class_counts.get(cls, 0) + 1
|
|
64
|
+
typer.echo(
|
|
65
|
+
json.dumps(
|
|
66
|
+
{
|
|
67
|
+
"osm_path": str(graph.osm_path),
|
|
68
|
+
"only_intersections": graph.only_intersections,
|
|
69
|
+
"trim_to_connected": graph.trim_to_connected,
|
|
70
|
+
"vertices": graph.vertex_count,
|
|
71
|
+
"edges": graph.edge_count,
|
|
72
|
+
"total_edge_length_km": round(sum(graph.edge_weight) / 1000.0, 3),
|
|
73
|
+
"edge_class_counts": {str(k): class_counts[k] for k in sorted(class_counts)},
|
|
74
|
+
"ref_lla": {"lat": graph.ref_lla.lat, "lon": graph.ref_lla.lon},
|
|
75
|
+
},
|
|
76
|
+
indent=1,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@geometry_app.command("materialize-plan")
|
|
82
|
+
def geometry_materialize_plan_cmd(
|
|
83
|
+
plan_path: Annotated[Path, typer.Argument(help="Group plan JSON (route_geometry.py contract).")],
|
|
84
|
+
repo_root: Annotated[Path, typer.Option("--repo-root", help="MAMUT-routing repo root the plan paths are relative to.")],
|
|
85
|
+
result_dir: Annotated[Optional[Path], typer.Option("--result-dir", help="Directory for per-group result files. Prints to stdout when omitted.")] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Materialize a route-geometry group plan (website build contract)."""
|
|
88
|
+
from mamut_routing_tools.geometry import materialize_plan
|
|
89
|
+
|
|
90
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
91
|
+
results = materialize_plan(repo_root, plan)
|
|
92
|
+
if result_dir is None:
|
|
93
|
+
typer.echo(json.dumps(results, sort_keys=True))
|
|
94
|
+
return
|
|
95
|
+
result_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
for result_file, payload in results.items():
|
|
97
|
+
target = result_dir / result_file
|
|
98
|
+
target.write_text(json.dumps(payload, sort_keys=True, separators=(",", ":")), encoding="utf-8")
|
|
99
|
+
typer.echo(str(target))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__": # pragma: no cover
|
|
103
|
+
app()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Geodetic conversions matching OpenStreetMapX.jl exactly.
|
|
2
|
+
|
|
3
|
+
The Julia pipeline stores road nodes as ENU coordinates linearized around the
|
|
4
|
+
center of the OSM bounds, computes edge lengths as 3D Euclidean distances in
|
|
5
|
+
that frame, and converts back to LLA for polylines. The Python engine keeps
|
|
6
|
+
the same math so graph weights and rendered coordinates agree to floating
|
|
7
|
+
point noise rather than to a model difference.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
from typing import NamedTuple
|
|
14
|
+
|
|
15
|
+
WGS84_A = 6378137.0
|
|
16
|
+
WGS84_B = 6356752.31424518
|
|
17
|
+
WGS84_E2 = 1.0 - (WGS84_B * WGS84_B) / (WGS84_A * WGS84_A)
|
|
18
|
+
WGS84_EP2 = (WGS84_A * WGS84_A) / (WGS84_B * WGS84_B) - 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LLA(NamedTuple):
|
|
22
|
+
lat: float
|
|
23
|
+
lon: float
|
|
24
|
+
alt: float = 0.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ECEF(NamedTuple):
|
|
28
|
+
x: float
|
|
29
|
+
y: float
|
|
30
|
+
z: float
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ENU(NamedTuple):
|
|
34
|
+
east: float
|
|
35
|
+
north: float
|
|
36
|
+
up: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ecef_from_lla(lla: LLA) -> ECEF:
|
|
40
|
+
lat_rad = math.radians(lla.lat)
|
|
41
|
+
lon_rad = math.radians(lla.lon)
|
|
42
|
+
sin_lat, cos_lat = math.sin(lat_rad), math.cos(lat_rad)
|
|
43
|
+
sin_lon, cos_lon = math.sin(lon_rad), math.cos(lon_rad)
|
|
44
|
+
n = WGS84_A / math.sqrt(1.0 - WGS84_E2 * sin_lat * sin_lat)
|
|
45
|
+
return ECEF(
|
|
46
|
+
(n + lla.alt) * cos_lat * cos_lon,
|
|
47
|
+
(n + lla.alt) * cos_lat * sin_lon,
|
|
48
|
+
(n * (1.0 - WGS84_E2) + lla.alt) * sin_lat,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def lla_from_ecef(ecef: ECEF) -> LLA:
|
|
53
|
+
x, y, z = ecef
|
|
54
|
+
p = math.hypot(x, y)
|
|
55
|
+
theta = math.atan2(z * WGS84_A, p * WGS84_B)
|
|
56
|
+
lon = math.atan2(y, x)
|
|
57
|
+
lat = math.atan2(
|
|
58
|
+
z + WGS84_EP2 * WGS84_B * math.sin(theta) ** 3,
|
|
59
|
+
p - WGS84_E2 * WGS84_A * math.cos(theta) ** 3,
|
|
60
|
+
)
|
|
61
|
+
n = WGS84_A / math.sqrt(1.0 - WGS84_E2 * math.sin(lat) ** 2)
|
|
62
|
+
alt = p / math.cos(lat) - n
|
|
63
|
+
return LLA(math.degrees(lat), math.degrees(lon), alt)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def enu_from_lla(lla: LLA, ref: LLA) -> ENU:
|
|
67
|
+
ecef = ecef_from_lla(lla)
|
|
68
|
+
ecef_ref = ecef_from_lla(ref)
|
|
69
|
+
dx, dy, dz = ecef.x - ecef_ref.x, ecef.y - ecef_ref.y, ecef.z - ecef_ref.z
|
|
70
|
+
lat_rad = math.radians(ref.lat)
|
|
71
|
+
lon_rad = math.radians(ref.lon)
|
|
72
|
+
sin_lat, cos_lat = math.sin(lat_rad), math.cos(lat_rad)
|
|
73
|
+
sin_lon, cos_lon = math.sin(lon_rad), math.cos(lon_rad)
|
|
74
|
+
east = -sin_lon * dx + cos_lon * dy
|
|
75
|
+
north = -cos_lon * sin_lat * dx - sin_lon * sin_lat * dy + cos_lat * dz
|
|
76
|
+
up = cos_lon * cos_lat * dx + sin_lon * cos_lat * dy + sin_lat * dz
|
|
77
|
+
return ENU(east, north, up)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def lla_from_enu(enu: ENU, ref: LLA) -> LLA:
|
|
81
|
+
lat_rad = math.radians(ref.lat)
|
|
82
|
+
lon_rad = math.radians(ref.lon)
|
|
83
|
+
sin_lat, cos_lat = math.sin(lat_rad), math.cos(lat_rad)
|
|
84
|
+
sin_lon, cos_lon = math.sin(lon_rad), math.cos(lon_rad)
|
|
85
|
+
east, north, up = enu
|
|
86
|
+
dx = -sin_lon * east - cos_lon * sin_lat * north + cos_lon * cos_lat * up
|
|
87
|
+
dy = cos_lon * east - sin_lon * sin_lat * north + sin_lon * cos_lat * up
|
|
88
|
+
dz = cos_lat * north + sin_lat * up
|
|
89
|
+
ecef_ref = ecef_from_lla(ref)
|
|
90
|
+
return lla_from_ecef(ECEF(ecef_ref.x + dx, ecef_ref.y + dy, ecef_ref.z + dz))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def enu_distance(a: ENU, b: ENU) -> float:
|
|
94
|
+
return math.sqrt((b.east - a.east) ** 2 + (b.north - a.north) ** 2 + (b.up - a.up) ** 2)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def bounds_center(min_lat: float, min_lon: float, max_lat: float, max_lon: float) -> LLA:
|
|
98
|
+
lon_mid = (min_lon + max_lon) / 2.0
|
|
99
|
+
lat_mid = (min_lat + max_lat) / 2.0
|
|
100
|
+
if min_lon > max_lon:
|
|
101
|
+
lon_mid = lon_mid - 180.0 if lon_mid > 0 else lon_mid + 180.0
|
|
102
|
+
return LLA(lat_mid, lon_mid)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
106
|
+
r = 6371000.0
|
|
107
|
+
dlat = math.radians(lat2 - lat1)
|
|
108
|
+
dlon = math.radians(lon2 - lon1)
|
|
109
|
+
a = math.sin(dlat / 2.0) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2.0) ** 2
|
|
110
|
+
return 2.0 * r * math.asin(math.sqrt(a))
|