"""Prepare the two downloaded Copernicus COGs; no browser/runtime GIS dependency. python -m pip install --target .tools/terrain -r scripts/terrain-requirements.txt python scripts/prepare-terrain.py """ import hashlib import json from pathlib import Path import subprocess import sys ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / '.tools' / 'terrain')) import numpy as np import rasterio from rasterio.merge import merge from rasterio.warp import reproject, Resampling from rasterio.transform import from_origin from pyproj import CRS, Transformer RADIUS = 12000 SPACING = 30 SIZE = 2 * RADIUS // SPACING + 1 ATTRIBUTION = ('produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and ' '© Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS ' 'by the European Union and ESA; all rights reserved') def main(): # The app remains the single source of observer coordinates. result = subprocess.run(['node', '--input-type=module', '-e', "import {observer} from './src/config/observer.ts'; console.log(JSON.stringify(observer))"], cwd=ROOT, capture_output=True, text=True, check=True) observer = json.loads(result.stdout) lat, lon = observer['latitude'], observer['longitude'] projection = f'+proj=aeqd +lat_0={lat} +lon_0={lon} +datum=WGS84 +units=m +no_defs' local_crs = CRS.from_proj4(projection) to_geo = Transformer.from_crs(local_crs, 'EPSG:4326', always_xy=True) transform = from_origin(-RADIUS - SPACING / 2, RADIUS + SPACING / 2, SPACING, SPACING) output = ROOT / 'public' / 'terrain' output.mkdir(parents=True, exist_ok=True) sources, manifest = [], [] try: for east in (126, 127): name = f'Copernicus_DSM_COG_10_N37_00_E{east}_00_DEM' path = ROOT / 'data' / 'raw' / 'copernicus' / f'{name}.tif' dataset = rasterio.open(path) sources.append(dataset) if dataset.crs.to_epsg() != 4326 or dataset.shape != (3600, 3600): raise ValueError(f'Unexpected source grid: {path}') manifest.append({ 'file': path.name, 'url': f'https://copernicus-dem-30m.s3.amazonaws.com/{name}/{name}.tif', 'bytes': path.stat().st_size, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'crs': dataset.crs.to_string(), 'bounds': list(dataset.bounds), 'shape': list(dataset.shape), 'pixelSizeDegrees': list(dataset.res), 'nodata': dataset.nodata, }) # Merge before resampling, so bilinear interpolation spans the 127° seam. mosaic, mosaic_transform = merge(sources, nodata=-9999) heights = np.full((SIZE, SIZE), -9999, dtype=np.float32) reproject(mosaic[0], heights, src_transform=mosaic_transform, src_crs=sources[0].crs, src_nodata=-9999, dst_transform=transform, dst_crs=local_crs, dst_nodata=-9999, resampling=Resampling.bilinear) missing = int(np.count_nonzero(~np.isfinite(heights) | (heights == -9999))) if missing: raise ValueError(f'Refusing to publish a grid with {missing} missing values') heights.astype('