| 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") |
| 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]) |
|
|
| |
| 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: |
| |
| bnds = clip_polygon_wgs84.bounds |
| 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 |
| 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: |
| |
| 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: |
| |
| 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: |
| |
| 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: |
| |
| 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, |
| ) |
|
|
| |
| |
| 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" |
| ) |
|
|