terrology / web /generate.py
Dav66's picture
Upload 2 files
7cc9e75 verified
Raw
History Blame Contribute Delete
13 kB
from __future__ import annotations
import json
import multiprocessing
import os
from datetime import UTC, datetime
from pathlib import Path
from terrology.cli import (
_bbox_with_buffer,
_chaikin_smooth,
_setup_utm,
)
from terrology.cli import (
make_shape_polygon as _make_shape_polygon,
)
from web.jobs import JobStatus, store
_JOB_DIR = Path(os.environ.get("TERROLOGY_JOB_DIR", "/tmp/terrology"))
def _worker(params_json: str, out_dir_str: str) -> None:
"""Runs in a spawned child process; writes _status.json on completion."""
out_dir = Path(out_dir_str)
out_dir.mkdir(parents=True, exist_ok=True)
status_file = out_dir / "_status.json"
try:
params = json.loads(params_json)
from terrology.cli import run_pipeline
clip_polygon_wgs84 = None
clip_polygon_utm = None
bbox_utm = None
route_points_utm = None
polygon_coords = params.get("polygon")
route_coords = params.get("route") # [[lon, lat], ...]
to_lat = params.get("to_lat")
span_buffer = params.get("span_buffer", 0.05)
if polygon_coords is not None:
from shapely.geometry import Polygon as _Polygon
from shapely.ops import transform as _shp_transform
clip_polygon_wgs84 = _Polygon([(c[0], c[1]) for c in polygon_coords])
# Apply Chaikin smoothing in WGS84 space (matches CLI behaviour)
smooth_n = params.get("smooth_boundary", 0)
if smooth_n > 0:
clip_polygon_wgs84 = _chaikin_smooth(clip_polygon_wgs84, smooth_n)
if route_coords is not None:
# Area + route mode
bnds = clip_polygon_wgs84.bounds # (min_lon, min_lat, max_lon, max_lat)
lat = (bnds[1] + bnds[3]) / 2
lon = (bnds[0] + bnds[2]) / 2
_, to_utm, _ = _setup_utm(lat, lon)
clip_polygon_utm = _shp_transform(
lambda x, y: to_utm.transform(x, y), clip_polygon_wgs84
)
ab = clip_polygon_utm.bounds # (x_min, y_min, x_max, y_max)
bbox_utm = _bbox_with_buffer(
[ab[0], ab[2]], [ab[1], ab[3]], span_buffer
)
route_points_utm = [
to_utm.transform(rlon, rlat) for rlon, rlat in route_coords
]
run_pipeline(
lat=lat,
lon=lon,
bbox_utm=bbox_utm,
clip_polygon_utm=clip_polygon_utm,
terrain_exag=params["terrain_exag"],
colors=params["colors"],
no_buildings=params.get("no_buildings", False),
no_terrain=params.get("no_terrain", False),
no_terrain_base=params.get("no_terrain_base", False),
roof_shapes=params.get("roof_shapes", False),
contour_interval=params.get("contour_interval"),
border_width_mm=params.get("border_width_mm", 0.0),
water_depth_mm=params.get("water_depth_mm", 0.8),
road_depth_mm=params.get("road_depth_mm", 0.15),
building_exag=params.get("building_exag"),
dem_source=params.get("dem_source", "glo30"),
raceway=params.get("raceway", False),
raceway_width=params.get("raceway_width", 1.5),
waterways=params.get("waterways", False),
waterway_width=params.get("waterway_width", 1.0),
route_points_utm=route_points_utm,
route_width=params.get("route_width", 1.5),
scale=params.get("scale"),
size=params.get("size", 190.0),
color_depth_mm=params.get("color_depth_mm", 1.5),
min_building_area=params.get("min_building_area"),
output_dir=out_dir,
color_grid_size=800,
)
else:
# Polygon-only mode: keep existing call path exactly for byte-identical output
centroid = clip_polygon_wgs84.centroid
lat = centroid.y
lon = centroid.x
radius = params.get("radius", 500)
run_pipeline(
lat=lat,
lon=lon,
radius=radius,
clip_polygon_wgs84=clip_polygon_wgs84,
terrain_exag=params["terrain_exag"],
colors=params["colors"],
no_buildings=params.get("no_buildings", False),
no_terrain=params.get("no_terrain", False),
no_terrain_base=params.get("no_terrain_base", False),
roof_shapes=params.get("roof_shapes", False),
contour_interval=params.get("contour_interval"),
border_width_mm=params.get("border_width_mm", 0.0),
water_depth_mm=params.get("water_depth_mm", 0.8),
road_depth_mm=params.get("road_depth_mm", 0.15),
building_exag=params.get("building_exag"),
dem_source=params.get("dem_source", "glo30"),
raceway=params.get("raceway", False),
raceway_width=params.get("raceway_width", 1.5),
waterways=params.get("waterways", False),
waterway_width=params.get("waterway_width", 1.0),
scale=params.get("scale"),
size=params.get("size", 190.0),
color_depth_mm=params.get("color_depth_mm", 1.5),
min_building_area=params.get("min_building_area"),
output_dir=out_dir,
color_grid_size=800,
)
elif route_coords is not None:
# Route-only mode
lons = [pt[0] for pt in route_coords]
lats = [pt[1] for pt in route_coords]
lat = (min(lats) + max(lats)) / 2
lon = (min(lons) + max(lons)) / 2
_, to_utm, _ = _setup_utm(lat, lon)
route_points_utm = [
to_utm.transform(rlon, rlat) for rlon, rlat in route_coords
]
xs = [p[0] for p in route_points_utm]
ys = [p[1] for p in route_points_utm]
bbox_utm = _bbox_with_buffer(xs, ys, span_buffer)
run_pipeline(
lat=lat,
lon=lon,
bbox_utm=bbox_utm,
terrain_exag=params["terrain_exag"],
colors=params["colors"],
no_buildings=params.get("no_buildings", False),
no_terrain=params.get("no_terrain", False),
no_terrain_base=params.get("no_terrain_base", False),
roof_shapes=params.get("roof_shapes", False),
contour_interval=params.get("contour_interval"),
border_width_mm=params.get("border_width_mm", 0.0),
water_depth_mm=params.get("water_depth_mm", 0.8),
road_depth_mm=params.get("road_depth_mm", 0.15),
building_exag=params.get("building_exag"),
dem_source=params.get("dem_source", "glo30"),
raceway=params.get("raceway", False),
raceway_width=params.get("raceway_width", 1.5),
waterways=params.get("waterways", False),
waterway_width=params.get("waterway_width", 1.0),
route_points_utm=route_points_utm,
route_width=params.get("route_width", 1.5),
scale=params.get("scale"),
size=params.get("size", 190.0),
color_depth_mm=params.get("color_depth_mm", 1.5),
min_building_area=params.get("min_building_area"),
output_dir=out_dir,
color_grid_size=800,
)
elif to_lat is not None:
# Two-point mode
lat1 = params["lat"]
lon1 = params["lon"]
lat2 = to_lat
lon2 = params["to_lon"]
lat = (lat1 + lat2) / 2
lon = (lon1 + lon2) / 2
_, to_utm, _ = _setup_utm(lat, lon)
x1, y1 = to_utm.transform(lon1, lat1)
x2, y2 = to_utm.transform(lon2, lat2)
bbox_utm = _bbox_with_buffer([x1, x2], [y1, y2], span_buffer)
run_pipeline(
lat=lat,
lon=lon,
bbox_utm=bbox_utm,
terrain_exag=params["terrain_exag"],
colors=params["colors"],
no_buildings=params.get("no_buildings", False),
no_terrain=params.get("no_terrain", False),
no_terrain_base=params.get("no_terrain_base", False),
roof_shapes=params.get("roof_shapes", False),
contour_interval=params.get("contour_interval"),
border_width_mm=params.get("border_width_mm", 0.0),
water_depth_mm=params.get("water_depth_mm", 0.8),
road_depth_mm=params.get("road_depth_mm", 0.15),
building_exag=params.get("building_exag"),
dem_source=params.get("dem_source", "glo30"),
raceway=params.get("raceway", False),
raceway_width=params.get("raceway_width", 1.5),
waterways=params.get("waterways", False),
waterway_width=params.get("waterway_width", 1.0),
scale=params.get("scale"),
size=params.get("size", 190.0),
color_depth_mm=params.get("color_depth_mm", 1.5),
min_building_area=params.get("min_building_area"),
output_dir=out_dir,
color_grid_size=800,
)
else:
# Pin mode (single lat/lon with radius)
lat = params["lat"]
lon = params["lon"]
radius = params["radius"]
shape = params.get("shape", "square")
if shape != "square":
clip_polygon_wgs84 = _make_shape_polygon(lat, lon, radius, shape)
run_pipeline(
lat=lat,
lon=lon,
radius=radius,
clip_polygon_wgs84=clip_polygon_wgs84,
terrain_exag=params["terrain_exag"],
colors=params["colors"],
no_buildings=params.get("no_buildings", False),
no_terrain=params.get("no_terrain", False),
no_terrain_base=params.get("no_terrain_base", False),
roof_shapes=params.get("roof_shapes", False),
contour_interval=params.get("contour_interval"),
border_width_mm=params.get("border_width_mm", 0.0),
water_depth_mm=params.get("water_depth_mm", 0.8),
road_depth_mm=params.get("road_depth_mm", 0.15),
building_exag=params.get("building_exag"),
dem_source=params.get("dem_source", "glo30"),
raceway=params.get("raceway", False),
raceway_width=params.get("raceway_width", 1.5),
waterways=params.get("waterways", False),
waterway_width=params.get("waterway_width", 1.0),
scale=params.get("scale"),
size=params.get("size", 190.0),
color_depth_mm=params.get("color_depth_mm", 1.5),
min_building_area=params.get("min_building_area"),
output_dir=out_dir,
color_grid_size=800,
)
status_file.write_text(json.dumps({"status": "ready"}))
except Exception as exc:
status_file.write_text(json.dumps({"status": "error", "error": str(exc)}))
def run_job(job_id: str, params: dict) -> None:
"""Called by FastAPI BackgroundTasks; runs run_pipeline in a spawned child process."""
out_dir = _JOB_DIR / job_id
store.update(
job_id,
status=JobStatus.RUNNING,
started_at=datetime.now(tz=UTC),
output_dir=out_dir,
)
# Spawn a fresh process so all pipeline memory is released when the job ends.
# "spawn" avoids inheriting asyncio state and thread locks from the server process.
ctx = multiprocessing.get_context("spawn")
p = ctx.Process(target=_worker, args=(json.dumps(params), str(out_dir)))
p.start()
p.join()
status_file = out_dir / "_status.json"
if status_file.exists():
data = json.loads(status_file.read_text())
if data.get("status") == "ready":
store.update(job_id, status=JobStatus.READY)
else:
store.update(
job_id,
status=JobStatus.ERROR,
error=data.get("error", "Unknown error"),
)
else:
store.update(
job_id, status=JobStatus.ERROR, error="Job process exited unexpectedly"
)