Skip to content

Commit 3379a2f

Browse files
authored
Merge pull request #89 from ssciwr/add_downsample_script
Add downsample_country_borders.py script here instead.
2 parents 6fafc31 + e0d8011 commit 3379a2f

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
"""
3+
A script I generated with AI to downsample the world country borders reference image we were provided to a suitable geojson size for frontend use.
4+
5+
I think as data updates do come to the other types of data (e.g. NUTS) which exist and are defined here on the backend, this makes sense to live here,
6+
but the exported image then gets used and saved on the frontent.
7+
8+
Downsample GeoJSON-like boundary files to a target max size.
9+
Usage example:
10+
python _scripts/downsample_geojson.py "World Bank Official Boundaries - Admin 0(1).png" --output "/
11+
downsample.geojson" --max-mb 6
12+
13+
14+
Example output:
15+
step=1 size=80140386
16+
step=2 size=41256344
17+
step=3 size=28353567
18+
step=4 size=21702896
19+
step=5 size=17520696
20+
step=6 size=14747263
21+
step=8 size=11331385
22+
step=10 size=9310428
23+
step=12 size=7977292
24+
step=15 size=6655828
25+
step=20 size=5353581
26+
27+
"""
28+
29+
from __future__ import annotations
30+
31+
import argparse
32+
import json
33+
from pathlib import Path
34+
35+
36+
def is_point(item: object) -> bool:
37+
return (
38+
isinstance(item, list)
39+
and len(item) >= 2
40+
and isinstance(item[0], (int, float))
41+
and isinstance(item[1], (int, float))
42+
)
43+
44+
45+
def simplify_line(line: list, step: int) -> list:
46+
if not line:
47+
return line
48+
points = line[::step] if step > 1 else line[:]
49+
if points[0] != line[0]:
50+
points.insert(0, line[0])
51+
if points[-1] != line[-1]:
52+
points.append(line[-1])
53+
return [[round(p[0], 5), round(p[1], 5)] + p[2:] for p in points]
54+
55+
56+
def simplify_coords(coords: object, step: int) -> object:
57+
if not isinstance(coords, list):
58+
return coords
59+
if coords and is_point(coords[0]):
60+
return simplify_line(coords, step)
61+
return [simplify_coords(item, step) for item in coords]
62+
63+
64+
def simplify_geom(geom: dict, step: int) -> dict:
65+
out = dict(geom)
66+
if "coordinates" in out:
67+
out["coordinates"] = simplify_coords(out["coordinates"], step)
68+
if "geometries" in out and isinstance(out["geometries"], list):
69+
out["geometries"] = [simplify_geom(item, step) for item in out["geometries"]]
70+
return out
71+
72+
73+
def downsample_geojson(data: dict, step: int) -> dict:
74+
out = dict(data)
75+
features = out.get("features")
76+
if isinstance(features, list):
77+
next_features = []
78+
for feature in features:
79+
updated = dict(feature)
80+
if isinstance(feature.get("geometry"), dict):
81+
updated["geometry"] = simplify_geom(feature["geometry"], step)
82+
next_features.append(updated)
83+
out["features"] = next_features
84+
return out
85+
86+
87+
def main() -> None:
88+
parser = argparse.ArgumentParser(description="Downsample boundary GeoJSON to target size.")
89+
parser.add_argument("input", type=Path, help="Input file path (GeoJSON text)")
90+
parser.add_argument("--output", type=Path, help="Output file path")
91+
parser.add_argument("--max-mb", type=float, default=6.0, help="Target max size in MB")
92+
args = parser.parse_args()
93+
94+
input_path = args.input
95+
output_path = args.output or input_path.with_name(f"{input_path.stem} - downsampled.geojson")
96+
max_bytes = int(args.max_mb * 1024 * 1024)
97+
98+
with input_path.open("r", encoding="utf-8") as f:
99+
data = json.load(f)
100+
101+
steps = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25, 30, 40, 50, 75, 100]
102+
for step in steps:
103+
output_data = downsample_geojson(data, step)
104+
text = json.dumps(output_data, separators=(",", ":"))
105+
106+
output_path.write_text(text, encoding="utf-8")
107+
size = output_path.stat().st_size
108+
print(f"step={step} size={size}")
109+
if size < max_bytes:
110+
print(f"output={output_path}")
111+
print(f"final_size_bytes={size}")
112+
return
113+
114+
raise SystemExit(f"Could not reduce file below {args.max_mb} MB")
115+
116+
117+
if __name__ == "__main__":
118+
main()

0 commit comments

Comments
 (0)