terraink-py 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.
- terraink_py-0.1.0/PKG-INFO +80 -0
- terraink_py-0.1.0/README.md +72 -0
- terraink_py-0.1.0/pyproject.toml +26 -0
- terraink_py-0.1.0/setup.cfg +4 -0
- terraink_py-0.1.0/src/terraink_py/__init__.py +14 -0
- terraink_py-0.1.0/src/terraink_py/__main__.py +5 -0
- terraink_py-0.1.0/src/terraink_py/api.py +86 -0
- terraink_py-0.1.0/src/terraink_py/cli.py +96 -0
- terraink_py-0.1.0/src/terraink_py/data/layouts.json +309 -0
- terraink_py-0.1.0/src/terraink_py/data/themes.json +454 -0
- terraink_py-0.1.0/src/terraink_py/data.py +84 -0
- terraink_py-0.1.0/src/terraink_py/geo.py +157 -0
- terraink_py-0.1.0/src/terraink_py/http.py +83 -0
- terraink_py-0.1.0/src/terraink_py/models.py +193 -0
- terraink_py-0.1.0/src/terraink_py/osm.py +477 -0
- terraink_py-0.1.0/src/terraink_py/render.py +857 -0
- terraink_py-0.1.0/src/terraink_py/text.py +45 -0
- terraink_py-0.1.0/src/terraink_py.egg-info/PKG-INFO +80 -0
- terraink_py-0.1.0/src/terraink_py.egg-info/SOURCES.txt +21 -0
- terraink_py-0.1.0/src/terraink_py.egg-info/dependency_links.txt +1 -0
- terraink_py-0.1.0/src/terraink_py.egg-info/entry_points.txt +2 -0
- terraink_py-0.1.0/src/terraink_py.egg-info/requires.txt +1 -0
- terraink_py-0.1.0/src/terraink_py.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: terraink_py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone Python CLI for generating styled OSM map posters as PNG and SVG.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: Pillow>=10.4.0
|
|
8
|
+
|
|
9
|
+
## Python CLI
|
|
10
|
+
|
|
11
|
+
The repo now also ships a standalone Python renderer that skips the website entirely and writes map posters directly to `png` and `svg`.
|
|
12
|
+
|
|
13
|
+
Use `uv` in this repo:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Generate a poster from a place name:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv run terraink \
|
|
23
|
+
"Ganjingzi District, China" \
|
|
24
|
+
--theme midnight_blue \
|
|
25
|
+
--layout print_a4_portrait \
|
|
26
|
+
--distance-m 4000 \
|
|
27
|
+
--format png svg \
|
|
28
|
+
--output outputs/ganjingzi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`--location "..."` still works; the positional argument is just a shorter alias.
|
|
32
|
+
|
|
33
|
+
If you want the CLI installed globally after publishing:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install terraink_py
|
|
37
|
+
terraink --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Build and publish to PyPI:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv build
|
|
44
|
+
uv publish
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Generate from coordinates in Python code:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
from terraink_py import PosterRequest, generate_poster
|
|
53
|
+
|
|
54
|
+
result = generate_poster(
|
|
55
|
+
PosterRequest(
|
|
56
|
+
output=Path("outputs/ganjingzi"),
|
|
57
|
+
formats=("png", "svg"),
|
|
58
|
+
lat=38.862405,
|
|
59
|
+
lon=121.513525,
|
|
60
|
+
title="Ganjingzi District",
|
|
61
|
+
subtitle="China",
|
|
62
|
+
theme="midnight_blue",
|
|
63
|
+
width_cm=21,
|
|
64
|
+
height_cm=29.7,
|
|
65
|
+
distance_m=4000,
|
|
66
|
+
include_buildings=True,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(result.files)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Notes:
|
|
74
|
+
|
|
75
|
+
- The Python renderer uses Nominatim + Overpass directly, so it is designed for city and regional posters rather than world-scale exports.
|
|
76
|
+
- `svg` output is true vector geometry, not a browser screenshot wrapped in SVG.
|
|
77
|
+
- Chinese place names now auto-fallback to common CJK system fonts on macOS/Linux; if your machine still lacks glyph coverage, pass `--font-file /path/to/font.ttf`.
|
|
78
|
+
- PNG export needs `Pillow`; `uv sync` will install it automatically.
|
|
79
|
+
- The PyPI package name is `terraink_py`, while the CLI command is `terraink`.
|
|
80
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
## Python CLI
|
|
2
|
+
|
|
3
|
+
The repo now also ships a standalone Python renderer that skips the website entirely and writes map posters directly to `png` and `svg`.
|
|
4
|
+
|
|
5
|
+
Use `uv` in this repo:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Generate a poster from a place name:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv run terraink \
|
|
15
|
+
"Ganjingzi District, China" \
|
|
16
|
+
--theme midnight_blue \
|
|
17
|
+
--layout print_a4_portrait \
|
|
18
|
+
--distance-m 4000 \
|
|
19
|
+
--format png svg \
|
|
20
|
+
--output outputs/ganjingzi
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`--location "..."` still works; the positional argument is just a shorter alias.
|
|
24
|
+
|
|
25
|
+
If you want the CLI installed globally after publishing:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv tool install terraink_py
|
|
29
|
+
terraink --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Build and publish to PyPI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv build
|
|
36
|
+
uv publish
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Generate from coordinates in Python code:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from terraink_py import PosterRequest, generate_poster
|
|
45
|
+
|
|
46
|
+
result = generate_poster(
|
|
47
|
+
PosterRequest(
|
|
48
|
+
output=Path("outputs/ganjingzi"),
|
|
49
|
+
formats=("png", "svg"),
|
|
50
|
+
lat=38.862405,
|
|
51
|
+
lon=121.513525,
|
|
52
|
+
title="Ganjingzi District",
|
|
53
|
+
subtitle="China",
|
|
54
|
+
theme="midnight_blue",
|
|
55
|
+
width_cm=21,
|
|
56
|
+
height_cm=29.7,
|
|
57
|
+
distance_m=4000,
|
|
58
|
+
include_buildings=True,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
print(result.files)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
|
|
67
|
+
- The Python renderer uses Nominatim + Overpass directly, so it is designed for city and regional posters rather than world-scale exports.
|
|
68
|
+
- `svg` output is true vector geometry, not a browser screenshot wrapped in SVG.
|
|
69
|
+
- Chinese place names now auto-fallback to common CJK system fonts on macOS/Linux; if your machine still lacks glyph coverage, pass `--font-file /path/to/font.ttf`.
|
|
70
|
+
- PNG export needs `Pillow`; `uv sync` will install it automatically.
|
|
71
|
+
- The PyPI package name is `terraink_py`, while the CLI command is `terraink`.
|
|
72
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "terraink_py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Standalone Python CLI for generating styled OSM map posters as PNG and SVG."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"Pillow>=10.4.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
terraink = "terraink_py.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
include-package-data = true
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.package-data]
|
|
22
|
+
terraink_py = ["data/*.json"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
26
|
+
include = ["terraink_py*"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .api import PosterGenerator, generate_poster
|
|
2
|
+
from .data import get_layout, get_theme, load_layouts, load_themes
|
|
3
|
+
from .models import PosterRequest, PosterResult
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"PosterGenerator",
|
|
7
|
+
"PosterRequest",
|
|
8
|
+
"PosterResult",
|
|
9
|
+
"generate_poster",
|
|
10
|
+
"get_layout",
|
|
11
|
+
"get_theme",
|
|
12
|
+
"load_layouts",
|
|
13
|
+
"load_themes",
|
|
14
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .data import get_layout, get_theme
|
|
6
|
+
from .geo import CM_PER_INCH, MercatorProjector, compute_poster_and_fetch_bounds, resolve_canvas_size
|
|
7
|
+
from .http import CachedHttpClient
|
|
8
|
+
from .models import Coordinate, PosterRequest, PosterResult
|
|
9
|
+
from .osm import fetch_osm_layers, resolve_location
|
|
10
|
+
from .render import build_scene, render_png, render_svg
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PosterGenerator:
|
|
14
|
+
def generate(self, request: PosterRequest) -> PosterResult:
|
|
15
|
+
prepared = prepare_request(request)
|
|
16
|
+
prepared.validate()
|
|
17
|
+
|
|
18
|
+
client = CachedHttpClient(
|
|
19
|
+
cache_dir=prepared.cache_dir,
|
|
20
|
+
user_agent=prepared.user_agent,
|
|
21
|
+
timeout_seconds=prepared.timeout_seconds,
|
|
22
|
+
)
|
|
23
|
+
location = resolve_location(prepared, client)
|
|
24
|
+
center = Coordinate(lat=location.lat, lon=location.lon)
|
|
25
|
+
size = resolve_canvas_size(
|
|
26
|
+
prepared.width_cm / CM_PER_INCH,
|
|
27
|
+
prepared.height_cm / CM_PER_INCH,
|
|
28
|
+
dpi=prepared.dpi,
|
|
29
|
+
max_pixels=prepared.max_pixels,
|
|
30
|
+
max_side=prepared.max_side,
|
|
31
|
+
)
|
|
32
|
+
bounds = compute_poster_and_fetch_bounds(
|
|
33
|
+
center=center,
|
|
34
|
+
distance_meters=prepared.distance_m,
|
|
35
|
+
aspect_ratio=prepared.width_cm / prepared.height_cm,
|
|
36
|
+
)
|
|
37
|
+
layers = fetch_osm_layers(bounds.fetch_bounds, prepared, client)
|
|
38
|
+
projector = MercatorProjector.from_bounds(bounds.poster_bounds, size.width, size.height)
|
|
39
|
+
theme = get_theme(prepared.theme)
|
|
40
|
+
scene = build_scene(
|
|
41
|
+
size=size,
|
|
42
|
+
center=center,
|
|
43
|
+
title=(prepared.title or location.city or location.label).strip(),
|
|
44
|
+
subtitle=(prepared.subtitle or location.country).strip(),
|
|
45
|
+
theme=theme,
|
|
46
|
+
layers=layers,
|
|
47
|
+
projector=projector,
|
|
48
|
+
request=prepared,
|
|
49
|
+
)
|
|
50
|
+
output_paths = resolve_output_paths(prepared.output, prepared.formats)
|
|
51
|
+
files: list[Path] = []
|
|
52
|
+
for fmt, path in output_paths.items():
|
|
53
|
+
if fmt == "png":
|
|
54
|
+
render_png(scene, path)
|
|
55
|
+
elif fmt == "svg":
|
|
56
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
path.write_text(render_svg(scene), encoding="utf-8")
|
|
58
|
+
files.append(path)
|
|
59
|
+
return PosterResult(
|
|
60
|
+
files=tuple(files),
|
|
61
|
+
location=location,
|
|
62
|
+
theme=theme,
|
|
63
|
+
size=size,
|
|
64
|
+
bounds=bounds,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def prepare_request(request: PosterRequest) -> PosterRequest:
|
|
69
|
+
if request.layout:
|
|
70
|
+
layout = get_layout(request.layout)
|
|
71
|
+
request.width_cm = layout.width_cm
|
|
72
|
+
request.height_cm = layout.height_cm
|
|
73
|
+
return request
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_output_paths(output: Path, formats: tuple[str, ...]) -> dict[str, Path]:
|
|
77
|
+
output = Path(output)
|
|
78
|
+
if len(formats) == 1 and output.suffix.lower() == f".{formats[0]}":
|
|
79
|
+
return {formats[0]: output}
|
|
80
|
+
|
|
81
|
+
base = output.with_suffix("") if output.suffix else output
|
|
82
|
+
return {fmt: base.with_suffix(f".{fmt}") for fmt in formats}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_poster(request: PosterRequest) -> PosterResult:
|
|
86
|
+
return PosterGenerator().generate(request)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .api import generate_poster
|
|
7
|
+
from .data import load_layouts, load_themes
|
|
8
|
+
from .models import DEFAULT_OVERPASS_URL, PosterRequest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
themes = ", ".join(load_themes().keys())
|
|
13
|
+
layouts = ", ".join(load_layouts().keys())
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="terraink",
|
|
16
|
+
description="Generate styled OpenStreetMap posters as PNG and SVG.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"query",
|
|
20
|
+
nargs="?",
|
|
21
|
+
help="Optional positional location query, equivalent to --location.",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--output", required=True, help="Output file path or base path.")
|
|
24
|
+
parser.add_argument("--format", nargs="+", default=["png"], choices=["png", "svg"])
|
|
25
|
+
parser.add_argument("--location", help="Location query for Nominatim geocoding.")
|
|
26
|
+
parser.add_argument("--lat", type=float, help="Latitude.")
|
|
27
|
+
parser.add_argument("--lon", type=float, help="Longitude.")
|
|
28
|
+
parser.add_argument("--title", help="Override poster title.")
|
|
29
|
+
parser.add_argument("--subtitle", help="Override poster subtitle.")
|
|
30
|
+
parser.add_argument("--theme", default="midnight_blue", help=f"Theme id. Available: {themes}")
|
|
31
|
+
parser.add_argument("--layout", help=f"Layout id. Available: {layouts}")
|
|
32
|
+
parser.add_argument("--width-cm", type=float, default=21.0, help="Poster width in centimeters.")
|
|
33
|
+
parser.add_argument("--height-cm", type=float, default=29.7, help="Poster height in centimeters.")
|
|
34
|
+
parser.add_argument("--distance-m", type=float, default=4000.0, help="Half-width map distance in meters.")
|
|
35
|
+
parser.add_argument("--dpi", type=int, default=300, help="Raster export DPI for PNG sizing.")
|
|
36
|
+
parser.add_argument("--font-file", type=Path, help="Optional TTF/OTF file used for PNG text rendering.")
|
|
37
|
+
parser.add_argument("--font-family", help="Font family name written into SVG text nodes.")
|
|
38
|
+
parser.add_argument("--cache-dir", type=Path, default=Path(".terraink-cache"))
|
|
39
|
+
parser.add_argument("--overpass-url", default=DEFAULT_OVERPASS_URL)
|
|
40
|
+
parser.add_argument("--nominatim-url", default="https://nominatim.openstreetmap.org")
|
|
41
|
+
parser.add_argument("--user-agent", default="terraink/0.1")
|
|
42
|
+
parser.add_argument("--timeout", type=int, default=90)
|
|
43
|
+
parser.add_argument("--hide-text", action="store_true", help="Do not draw poster title/subtitle/coords.")
|
|
44
|
+
parser.add_argument("--hide-credits", action="store_true", help="Do not draw footer credits.")
|
|
45
|
+
parser.add_argument("--include-buildings", action=argparse.BooleanOptionalAction, default=False)
|
|
46
|
+
parser.add_argument("--include-water", action=argparse.BooleanOptionalAction, default=True)
|
|
47
|
+
parser.add_argument("--include-parks", action=argparse.BooleanOptionalAction, default=True)
|
|
48
|
+
parser.add_argument("--include-aeroway", action=argparse.BooleanOptionalAction, default=True)
|
|
49
|
+
parser.add_argument("--include-rail", action=argparse.BooleanOptionalAction, default=True)
|
|
50
|
+
parser.add_argument("--include-roads", action=argparse.BooleanOptionalAction, default=True)
|
|
51
|
+
parser.add_argument("--include-road-path", action=argparse.BooleanOptionalAction, default=True)
|
|
52
|
+
parser.add_argument("--include-road-minor-low", action=argparse.BooleanOptionalAction, default=True)
|
|
53
|
+
parser.add_argument("--include-road-outline", action=argparse.BooleanOptionalAction, default=True)
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(argv: list[str] | None = None) -> int:
|
|
58
|
+
parser = build_parser()
|
|
59
|
+
args = parser.parse_args(argv)
|
|
60
|
+
request = PosterRequest(
|
|
61
|
+
output=Path(args.output),
|
|
62
|
+
formats=tuple(args.format),
|
|
63
|
+
location=args.location or args.query,
|
|
64
|
+
lat=args.lat,
|
|
65
|
+
lon=args.lon,
|
|
66
|
+
title=args.title,
|
|
67
|
+
subtitle=args.subtitle,
|
|
68
|
+
width_cm=args.width_cm,
|
|
69
|
+
height_cm=args.height_cm,
|
|
70
|
+
distance_m=args.distance_m,
|
|
71
|
+
dpi=args.dpi,
|
|
72
|
+
theme=args.theme,
|
|
73
|
+
layout=args.layout,
|
|
74
|
+
font_file=args.font_file,
|
|
75
|
+
font_family=args.font_family,
|
|
76
|
+
show_poster_text=not args.hide_text,
|
|
77
|
+
include_credits=not args.hide_credits,
|
|
78
|
+
include_buildings=args.include_buildings,
|
|
79
|
+
include_water=args.include_water,
|
|
80
|
+
include_parks=args.include_parks,
|
|
81
|
+
include_aeroway=args.include_aeroway,
|
|
82
|
+
include_rail=args.include_rail,
|
|
83
|
+
include_roads=args.include_roads,
|
|
84
|
+
include_road_path=args.include_road_path,
|
|
85
|
+
include_road_minor_low=args.include_road_minor_low,
|
|
86
|
+
include_road_outline=args.include_road_outline,
|
|
87
|
+
cache_dir=args.cache_dir,
|
|
88
|
+
overpass_url=args.overpass_url,
|
|
89
|
+
nominatim_url=args.nominatim_url,
|
|
90
|
+
user_agent=args.user_agent,
|
|
91
|
+
timeout_seconds=args.timeout,
|
|
92
|
+
)
|
|
93
|
+
result = generate_poster(request)
|
|
94
|
+
for path in result.files:
|
|
95
|
+
print(path)
|
|
96
|
+
return 0
|