yoro-maps 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.
- yoro_maps-0.1.0/.github/workflows/publish.yml +45 -0
- yoro_maps-0.1.0/.github/workflows/tests.yml +25 -0
- yoro_maps-0.1.0/.gitignore +11 -0
- yoro_maps-0.1.0/LICENSE +21 -0
- yoro_maps-0.1.0/PKG-INFO +93 -0
- yoro_maps-0.1.0/README.md +58 -0
- yoro_maps-0.1.0/pyproject.toml +56 -0
- yoro_maps-0.1.0/src/yoromaps/__init__.py +36 -0
- yoro_maps-0.1.0/src/yoromaps/cli.py +139 -0
- yoro_maps-0.1.0/src/yoromaps/countries.py +54 -0
- yoro_maps-0.1.0/src/yoromaps/db.py +112 -0
- yoro_maps-0.1.0/src/yoromaps/django/__init__.py +18 -0
- yoro_maps-0.1.0/src/yoromaps/django/apps.py +8 -0
- yoro_maps-0.1.0/src/yoromaps/django/router.py +31 -0
- yoro_maps-0.1.0/src/yoromaps/django/urls.py +10 -0
- yoro_maps-0.1.0/src/yoromaps/django/views.py +90 -0
- yoro_maps-0.1.0/src/yoromaps/download.py +101 -0
- yoro_maps-0.1.0/src/yoromaps/extract.py +148 -0
- yoro_maps-0.1.0/src/yoromaps/routing.py +221 -0
- yoro_maps-0.1.0/src/yoromaps/tiles.py +106 -0
- yoro_maps-0.1.0/tests/__init__.py +0 -0
- yoro_maps-0.1.0/tests/test_core.py +210 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
|
|
18
|
+
- name: Install build tools
|
|
19
|
+
run: pip install build
|
|
20
|
+
|
|
21
|
+
- name: Build wheel and sdist
|
|
22
|
+
run: python -m build
|
|
23
|
+
|
|
24
|
+
- name: Upload artifacts
|
|
25
|
+
uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
name: Publish to PyPI
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment: pypi
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- name: Download artifacts
|
|
39
|
+
uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
- name: Publish to PyPI
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: pytest tests/ -v
|
yoro_maps-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Guindo / Altius Academy SNC
|
|
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.
|
yoro_maps-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yoro-maps
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline maps, routing, and POI for Africa — companion to yoro geocoding
|
|
5
|
+
Project-URL: Homepage, https://github.com/Altius-Academy-SNC/yoro-maps
|
|
6
|
+
Project-URL: Repository, https://github.com/Altius-Academy-SNC/yoro-maps
|
|
7
|
+
Author-email: Paul Guindo <paulguindo@altius-group.ch>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: africa,maps,mbtiles,offline,osm,routing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx>=0.24
|
|
19
|
+
Requires-Dist: yoro>=0.2.0
|
|
20
|
+
Provides-Extra: all
|
|
21
|
+
Requires-Dist: django>=4.2; extra == 'all'
|
|
22
|
+
Requires-Dist: mercantile>=1.2; extra == 'all'
|
|
23
|
+
Requires-Dist: pyosmium>=3.6; extra == 'all'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Provides-Extra: django
|
|
29
|
+
Requires-Dist: django>=4.2; extra == 'django'
|
|
30
|
+
Provides-Extra: extract
|
|
31
|
+
Requires-Dist: pyosmium>=3.6; extra == 'extract'
|
|
32
|
+
Provides-Extra: tiles
|
|
33
|
+
Requires-Dist: mercantile>=1.2; extra == 'tiles'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Yoro Maps
|
|
37
|
+
|
|
38
|
+
Offline maps, routing, and POI for Africa — companion to [yoro](https://pypi.org/project/yoro/) geocoding.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install yoro-maps
|
|
44
|
+
pip install yoro-maps[django] # Django integration
|
|
45
|
+
pip install yoro-maps[tiles] # Tile downloading
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Build a map database
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Download OSM data and build road graph for Mali
|
|
52
|
+
yoromaps download ML --output mali.yoromaps
|
|
53
|
+
|
|
54
|
+
# With map tiles (slower, ~200+ MB)
|
|
55
|
+
yoromaps download ML --output mali.yoromaps --tiles --zoom-max 14
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Route between Yoro codes
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
yoromaps route mali.yoromaps ML-ABC ML-XYZ
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import yoromaps
|
|
66
|
+
|
|
67
|
+
conn = yoromaps.open_db("mali.yoromaps")
|
|
68
|
+
result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
|
|
69
|
+
print(f"{result.distance_km} km, {result.duration_min} min")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Django integration
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# settings.py
|
|
76
|
+
import yoromaps
|
|
77
|
+
|
|
78
|
+
DATABASES = {
|
|
79
|
+
"default": { ... },
|
|
80
|
+
"maps": yoromaps.db_config("/data/mali.yoromaps"),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
DATABASE_ROUTERS = ["yoromaps.django.router.YoroMapsRouter"]
|
|
84
|
+
|
|
85
|
+
# urls.py
|
|
86
|
+
urlpatterns = [
|
|
87
|
+
path("maps/", include("yoromaps.django.urls")),
|
|
88
|
+
]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT — Paul Guindo / Altius Academy SNC.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Yoro Maps
|
|
2
|
+
|
|
3
|
+
Offline maps, routing, and POI for Africa — companion to [yoro](https://pypi.org/project/yoro/) geocoding.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install yoro-maps
|
|
9
|
+
pip install yoro-maps[django] # Django integration
|
|
10
|
+
pip install yoro-maps[tiles] # Tile downloading
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Build a map database
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Download OSM data and build road graph for Mali
|
|
17
|
+
yoromaps download ML --output mali.yoromaps
|
|
18
|
+
|
|
19
|
+
# With map tiles (slower, ~200+ MB)
|
|
20
|
+
yoromaps download ML --output mali.yoromaps --tiles --zoom-max 14
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Route between Yoro codes
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yoromaps route mali.yoromaps ML-ABC ML-XYZ
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import yoromaps
|
|
31
|
+
|
|
32
|
+
conn = yoromaps.open_db("mali.yoromaps")
|
|
33
|
+
result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
|
|
34
|
+
print(f"{result.distance_km} km, {result.duration_min} min")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Django integration
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
# settings.py
|
|
41
|
+
import yoromaps
|
|
42
|
+
|
|
43
|
+
DATABASES = {
|
|
44
|
+
"default": { ... },
|
|
45
|
+
"maps": yoromaps.db_config("/data/mali.yoromaps"),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
DATABASE_ROUTERS = ["yoromaps.django.router.YoroMapsRouter"]
|
|
49
|
+
|
|
50
|
+
# urls.py
|
|
51
|
+
urlpatterns = [
|
|
52
|
+
path("maps/", include("yoromaps.django.urls")),
|
|
53
|
+
]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT — Paul Guindo / Altius Academy SNC.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yoro-maps"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Offline maps, routing, and POI for Africa — companion to yoro geocoding"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Paul Guindo", email = "paulguindo@altius-group.ch" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["maps", "routing", "offline", "africa", "osm", "mbtiles"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
22
|
+
"Framework :: Django",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"yoro>=0.2.0",
|
|
26
|
+
"httpx>=0.24",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
django = ["Django>=4.2"]
|
|
31
|
+
tiles = ["mercantile>=1.2"]
|
|
32
|
+
extract = ["pyosmium>=3.6"]
|
|
33
|
+
all = ["yoro-maps[django,tiles,extract]"]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7",
|
|
36
|
+
"pytest-cov",
|
|
37
|
+
"ruff",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/Altius-Academy-SNC/yoro-maps"
|
|
42
|
+
Repository = "https://github.com/Altius-Academy-SNC/yoro-maps"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
yoromaps = "yoromaps.cli:main"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/yoromaps"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
pythonpath = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
line-length = 100
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yoro Maps — Offline maps, routing, and POI for Africa.
|
|
3
|
+
|
|
4
|
+
Companion to the ``yoro`` geocoding package.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
import yoromaps
|
|
9
|
+
|
|
10
|
+
# Build a .yoromaps file for Mali
|
|
11
|
+
yoromaps.build("ML", "mali.yoromaps")
|
|
12
|
+
|
|
13
|
+
# Route between Yoro codes
|
|
14
|
+
conn = yoromaps.open_db("mali.yoromaps")
|
|
15
|
+
result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
|
|
16
|
+
print(f"{result.distance_km} km, {result.duration_min} min")
|
|
17
|
+
|
|
18
|
+
# Route from Yoro codes
|
|
19
|
+
legs = yoromaps.route_from_codes(conn, ["ML-ABC", "ML-XYZ"])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from yoromaps.db import db_config, open_db
|
|
23
|
+
from yoromaps.download import build
|
|
24
|
+
from yoromaps.routing import RouteResult, route, route_from_codes
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"build",
|
|
30
|
+
"open_db",
|
|
31
|
+
"db_config",
|
|
32
|
+
"route",
|
|
33
|
+
"route_from_codes",
|
|
34
|
+
"RouteResult",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for yoro-maps.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
yoromaps download ML --output mali.yoromaps
|
|
7
|
+
yoromaps download ML --output mali.yoromaps --tiles --zoom-max 14
|
|
8
|
+
yoromaps route mali.yoromaps ML-ABC ML-XYZ
|
|
9
|
+
yoromaps info mali.yoromaps
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from yoromaps.countries import COUNTRIES
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _progress(msg, current, total):
|
|
22
|
+
if total > 0:
|
|
23
|
+
pct = current / total * 100
|
|
24
|
+
print(f"\r {msg}: {pct:.0f}% ({current}/{total})", end="", flush=True)
|
|
25
|
+
else:
|
|
26
|
+
print(f"\r {msg}", end="", flush=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _download(args):
|
|
30
|
+
from yoromaps.download import build
|
|
31
|
+
|
|
32
|
+
code = args.country.upper()
|
|
33
|
+
if code not in COUNTRIES:
|
|
34
|
+
print(f"Unknown country: {code}. Available: {', '.join(sorted(COUNTRIES))}", file=sys.stderr)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
output = args.output or f"{code.lower()}.yoromaps"
|
|
38
|
+
print(f"Building {output} for {COUNTRIES[code].name}...")
|
|
39
|
+
|
|
40
|
+
build(
|
|
41
|
+
code,
|
|
42
|
+
output=output,
|
|
43
|
+
pbf_path=args.pbf,
|
|
44
|
+
include_tiles=args.tiles,
|
|
45
|
+
zoom_min=args.zoom_min,
|
|
46
|
+
zoom_max=args.zoom_max,
|
|
47
|
+
progress=_progress,
|
|
48
|
+
)
|
|
49
|
+
print(f"\nDone: {output}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _route(args):
|
|
53
|
+
from yoromaps.db import open_db
|
|
54
|
+
from yoromaps.routing import route_from_codes
|
|
55
|
+
|
|
56
|
+
conn = open_db(args.db)
|
|
57
|
+
legs = route_from_codes(conn, args.codes)
|
|
58
|
+
conn.close()
|
|
59
|
+
|
|
60
|
+
total_km = sum(leg.distance_km for leg in legs)
|
|
61
|
+
total_min = sum(leg.duration_min for leg in legs)
|
|
62
|
+
|
|
63
|
+
result = {
|
|
64
|
+
"total_distance_km": total_km,
|
|
65
|
+
"total_duration_min": total_min,
|
|
66
|
+
"legs": [
|
|
67
|
+
{
|
|
68
|
+
"distance_km": leg.distance_km,
|
|
69
|
+
"duration_min": leg.duration_min,
|
|
70
|
+
"found": leg.found,
|
|
71
|
+
"steps": leg.steps,
|
|
72
|
+
}
|
|
73
|
+
for leg in legs
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
print(json.dumps(result, indent=2))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _info(args):
|
|
80
|
+
from yoromaps.db import open_db, get_metadata
|
|
81
|
+
from yoromaps.tiles import tile_count
|
|
82
|
+
|
|
83
|
+
conn = open_db(args.db)
|
|
84
|
+
print(f"Database: {args.db}")
|
|
85
|
+
for key in ("country", "country_name", "bbox", "graph_nodes", "graph_edges", "tiles_count"):
|
|
86
|
+
val = get_metadata(conn, key)
|
|
87
|
+
if val:
|
|
88
|
+
print(f" {key}: {val}")
|
|
89
|
+
tc = tile_count(conn)
|
|
90
|
+
if tc:
|
|
91
|
+
print(f" tiles_in_db: {tc}")
|
|
92
|
+
conn.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _countries(args):
|
|
96
|
+
for code, c in sorted(COUNTRIES.items()):
|
|
97
|
+
print(f" {code} {c.name}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main():
|
|
101
|
+
parser = argparse.ArgumentParser(prog="yoromaps", description="Offline maps & routing for Africa")
|
|
102
|
+
sub = parser.add_subparsers(dest="command")
|
|
103
|
+
|
|
104
|
+
# download
|
|
105
|
+
dl = sub.add_parser("download", help="Download and build a .yoromaps file")
|
|
106
|
+
dl.add_argument("country", help="ISO country code (e.g. ML, BF, CI)")
|
|
107
|
+
dl.add_argument("--output", "-o", help="Output file path")
|
|
108
|
+
dl.add_argument("--pbf", help="Use existing PBF file instead of downloading")
|
|
109
|
+
dl.add_argument("--tiles", action="store_true", help="Also download map tiles")
|
|
110
|
+
dl.add_argument("--zoom-min", type=int, default=6)
|
|
111
|
+
dl.add_argument("--zoom-max", type=int, default=12)
|
|
112
|
+
|
|
113
|
+
# route
|
|
114
|
+
rt = sub.add_parser("route", help="Calculate route between Yoro codes")
|
|
115
|
+
rt.add_argument("db", help="Path to .yoromaps file")
|
|
116
|
+
rt.add_argument("codes", nargs="+", help="Yoro codes (e.g. ML-ABC ML-XYZ)")
|
|
117
|
+
|
|
118
|
+
# info
|
|
119
|
+
nf = sub.add_parser("info", help="Show database info")
|
|
120
|
+
nf.add_argument("db", help="Path to .yoromaps file")
|
|
121
|
+
|
|
122
|
+
# countries
|
|
123
|
+
sub.add_parser("countries", help="List available countries")
|
|
124
|
+
|
|
125
|
+
args = parser.parse_args()
|
|
126
|
+
if args.command == "download":
|
|
127
|
+
_download(args)
|
|
128
|
+
elif args.command == "route":
|
|
129
|
+
_route(args)
|
|
130
|
+
elif args.command == "info":
|
|
131
|
+
_info(args)
|
|
132
|
+
elif args.command == "countries":
|
|
133
|
+
_countries(args)
|
|
134
|
+
else:
|
|
135
|
+
parser.print_help()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Country registry — Geofabrik download URLs and metadata.
|
|
3
|
+
|
|
4
|
+
Focused on Africa, extensible to other regions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class Country:
|
|
14
|
+
code: str
|
|
15
|
+
name: str
|
|
16
|
+
geofabrik_path: str
|
|
17
|
+
bbox: tuple[float, float, float, float] # lon_min, lat_min, lon_max, lat_max
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
COUNTRIES: dict[str, Country] = {
|
|
21
|
+
# --- West Africa (priority) ---
|
|
22
|
+
"ML": Country("ML", "Mali", "africa/mali", (-12.5, 10.0, 4.5, 25.0)),
|
|
23
|
+
"NE": Country("NE", "Niger", "africa/niger", (0.0, 11.5, 16.0, 24.0)),
|
|
24
|
+
"BF": Country("BF", "Burkina Faso", "africa/burkina-faso", (-5.5, 9.0, 2.5, 15.5)),
|
|
25
|
+
"CI": Country("CI", "Cote d'Ivoire", "africa/ivory-coast", (-8.6, 4.36, -2.49, 10.74)),
|
|
26
|
+
"GH": Country("GH", "Ghana", "africa/ghana", (-3.26, 4.74, 1.2, 11.17)),
|
|
27
|
+
"SN": Country("SN", "Senegal", "africa/senegal-and-gambia", (-17.54, 12.31, -11.36, 16.69)),
|
|
28
|
+
"GN": Country("GN", "Guinee", "africa/guinea", (-15.08, 7.19, -7.64, 12.68)),
|
|
29
|
+
"TG": Country("TG", "Togo", "africa/togo", (-0.15, 6.1, 1.81, 11.14)),
|
|
30
|
+
"BJ": Country("BJ", "Benin", "africa/benin", (0.77, 6.14, 3.85, 12.41)),
|
|
31
|
+
"NG": Country("NG", "Nigeria", "africa/nigeria", (2.69, 4.27, 14.68, 13.89)),
|
|
32
|
+
"CM": Country("CM", "Cameroun", "africa/cameroon", (8.49, 1.65, 16.19, 13.08)),
|
|
33
|
+
"SL": Country("SL", "Sierra Leone", "africa/sierra-leone", (-13.3, 6.93, -10.27, 10.0)),
|
|
34
|
+
"LR": Country("LR", "Liberia", "africa/liberia", (-11.49, 4.35, -7.37, 8.55)),
|
|
35
|
+
"MR": Country("MR", "Mauritanie", "africa/mauritania", (-17.07, 14.72, -4.83, 27.3)),
|
|
36
|
+
"GW": Country("GW", "Guinee-Bissau", "africa/guinea-bissau", (-16.71, 10.87, -13.64, 12.68)),
|
|
37
|
+
# --- Central / East Africa ---
|
|
38
|
+
"CD": Country("CD", "RD Congo", "africa/congo-democratic-republic", (12.18, -13.46, 31.31, 5.39)),
|
|
39
|
+
"KE": Country("KE", "Kenya", "africa/kenya", (33.91, -4.68, 41.91, 5.03)),
|
|
40
|
+
"TZ": Country("TZ", "Tanzanie", "africa/tanzania", (29.33, -11.75, 40.44, -0.99)),
|
|
41
|
+
"UG": Country("UG", "Ouganda", "africa/uganda", (29.57, -1.48, 35.04, 4.23)),
|
|
42
|
+
"ET": Country("ET", "Ethiopie", "africa/ethiopia", (32.99, 3.4, 47.99, 14.89)),
|
|
43
|
+
"MG": Country("MG", "Madagascar", "africa/madagascar", (43.18, -25.61, 50.48, -11.95)),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
GEOFABRIK_BASE = "https://download.geofabrik.de"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def geofabrik_url(country_code: str) -> str:
|
|
50
|
+
"""Return the Geofabrik PBF download URL for a country."""
|
|
51
|
+
c = COUNTRIES.get(country_code.upper())
|
|
52
|
+
if not c:
|
|
53
|
+
raise ValueError(f"Unknown country: {country_code}. Available: {list(COUNTRIES.keys())}")
|
|
54
|
+
return f"{GEOFABRIK_BASE}/{c.geofabrik_path}-latest.osm.pbf"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database management for .yoromaps files.
|
|
3
|
+
|
|
4
|
+
A .yoromaps file is a single SQLite database containing:
|
|
5
|
+
- tiles: MBTiles-compatible tile storage
|
|
6
|
+
- nodes: road graph intersections
|
|
7
|
+
- edges: road graph segments
|
|
8
|
+
- pois: points of interest
|
|
9
|
+
- metadata: version, country, timestamps
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sqlite3
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
SCHEMA_SQL = """
|
|
20
|
+
-- Metadata
|
|
21
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
22
|
+
key TEXT PRIMARY KEY,
|
|
23
|
+
value TEXT
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- MBTiles-compatible tile storage
|
|
27
|
+
CREATE TABLE IF NOT EXISTS tiles (
|
|
28
|
+
zoom_level INTEGER NOT NULL,
|
|
29
|
+
tile_column INTEGER NOT NULL,
|
|
30
|
+
tile_row INTEGER NOT NULL,
|
|
31
|
+
tile_data BLOB NOT NULL,
|
|
32
|
+
PRIMARY KEY (zoom_level, tile_column, tile_row)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Road graph: nodes (intersections)
|
|
36
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
37
|
+
id INTEGER PRIMARY KEY,
|
|
38
|
+
lat REAL NOT NULL,
|
|
39
|
+
lon REAL NOT NULL
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
-- Road graph: edges (road segments)
|
|
43
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
from_node INTEGER NOT NULL REFERENCES nodes(id),
|
|
46
|
+
to_node INTEGER NOT NULL REFERENCES nodes(id),
|
|
47
|
+
distance_m REAL NOT NULL,
|
|
48
|
+
duration_s REAL NOT NULL,
|
|
49
|
+
road_type TEXT NOT NULL DEFAULT 'road',
|
|
50
|
+
oneway INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
name TEXT DEFAULT ''
|
|
52
|
+
);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node);
|
|
55
|
+
|
|
56
|
+
-- Points of interest
|
|
57
|
+
CREATE TABLE IF NOT EXISTS pois (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
lat REAL NOT NULL,
|
|
60
|
+
lon REAL NOT NULL,
|
|
61
|
+
yoro_code TEXT NOT NULL DEFAULT '',
|
|
62
|
+
name TEXT NOT NULL,
|
|
63
|
+
category TEXT NOT NULL DEFAULT 'other',
|
|
64
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
65
|
+
osm_id INTEGER,
|
|
66
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
67
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
68
|
+
);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_pois_yoro ON pois(yoro_code);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_pois_category ON pois(category);
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def open_db(path: str | Path, create: bool = False) -> sqlite3.Connection:
|
|
75
|
+
"""Open a .yoromaps database. Creates schema if *create* is True."""
|
|
76
|
+
path = Path(path)
|
|
77
|
+
exists = path.exists()
|
|
78
|
+
|
|
79
|
+
if not exists and not create:
|
|
80
|
+
raise FileNotFoundError(f"Database not found: {path}")
|
|
81
|
+
|
|
82
|
+
conn = sqlite3.connect(str(path))
|
|
83
|
+
conn.row_factory = sqlite3.Row
|
|
84
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
85
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
86
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
87
|
+
|
|
88
|
+
if create and not exists:
|
|
89
|
+
conn.executescript(SCHEMA_SQL)
|
|
90
|
+
conn.execute("INSERT OR REPLACE INTO metadata VALUES ('schema_version', ?)",
|
|
91
|
+
(str(SCHEMA_VERSION),))
|
|
92
|
+
conn.commit()
|
|
93
|
+
|
|
94
|
+
return conn
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def set_metadata(conn: sqlite3.Connection, key: str, value: str) -> None:
|
|
98
|
+
conn.execute("INSERT OR REPLACE INTO metadata VALUES (?, ?)", (key, value))
|
|
99
|
+
conn.commit()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_metadata(conn: sqlite3.Connection, key: str) -> str | None:
|
|
103
|
+
row = conn.execute("SELECT value FROM metadata WHERE key = ?", (key,)).fetchone()
|
|
104
|
+
return row[0] if row else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def db_config(path: str) -> dict:
|
|
108
|
+
"""Return a Django DATABASES dict entry for a .yoromaps file."""
|
|
109
|
+
return {
|
|
110
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
111
|
+
"NAME": str(path),
|
|
112
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yoro Maps Django integration.
|
|
3
|
+
|
|
4
|
+
Add to your settings::
|
|
5
|
+
|
|
6
|
+
import yoromaps
|
|
7
|
+
|
|
8
|
+
INSTALLED_APPS = [..., "yoromaps.django"]
|
|
9
|
+
|
|
10
|
+
DATABASES = {
|
|
11
|
+
"default": { ... },
|
|
12
|
+
"maps": yoromaps.db_config("/path/to/country.yoromaps"),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
DATABASE_ROUTERS = ["yoromaps.django.router.YoroMapsRouter"]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
default_app_config = "yoromaps.django.apps.YoroMapsConfig"
|