"""Extract Hajodae/Gisamun DSM and OSM coast using .tools/terrain dependencies. Run: python scripts/prepare-hajodae.py [hajodae|gisamun] The 10m grid is interpolated from 30m DSM, not higher-accuracy survey data. """ import hashlib import json from pathlib import Path import subprocess import sys from contextlib import ExitStack ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / '.tools/terrain')) import numpy as np import rasterio from rasterio.features import rasterize from rasterio.merge import merge from rasterio.transform import from_origin from rasterio.warp import reproject, Resampling from pyproj import Transformer def main(): site = sys.argv[1] if len(sys.argv) > 1 else 'hajodae' if site not in ('hajodae', 'gisamun'): raise ValueError('Expected hajodae or gisamun') config = json.loads(subprocess.check_output(['node', '--input-type=module', '-e', "import {COAST} from './src/constants/coast.ts'; console.log(JSON.stringify(COAST))"], cwd=ROOT)) if site == 'gisamun': config.update(json.loads(subprocess.check_output(['node', '--input-type=module', '-e', "import {GISAMUN} from './src/constants/gisamun.ts'; console.log(JSON.stringify(GISAMUN))"], cwd=ROOT))) lat, lon = config['latitude'], config['longitude'] radius, step = config['radiusMeters'], config['spacingMeters'] size = 2 * radius // step + 1 projection = f'+proj=aeqd +lat_0={lat} +lon_0={lon} +datum=WGS84 +units=m +no_defs' forward = Transformer.from_crs('EPSG:4326', projection, always_xy=True) inverse = Transformer.from_crs(projection, 'EPSG:4326', always_xy=True) features = json.loads((ROOT / f'data/references/{site}/coastal-features.geojson').read_text(encoding='utf-8'))['features'] coast = [f for f in features if f['properties']['category'] == 'coastline'] mainland = next(f['geometry']['coordinates'] for f in coast if f['id'] == 'way/583161999') mainland_ids = {'way/583161999'} if site == 'gisamun': south = next(f['geometry']['coordinates'] for f in coast if f['id'] == 'way/22784123') assert south[-1] == mainland[0], 'Mainland segments must join exactly' mainland = south[:-1] + mainland mainland_ids.add('way/22784123') # This source runs south -> north; land is on its west side. Close well west # of the crop. These artificial closing edges never cross the published area. mainland = mainland + [[128.6, mainland[-1][1]], [128.6, mainland[0][1]], mainland[0]] geographic_land = [mainland] + [f['geometry']['coordinates'] for f in coast if f['id'] not in mainland_ids] beaches = [f['geometry']['coordinates'][0] for f in features if f['properties']['category'] == 'beach'] def project(ring): return [list(forward.transform(*p)) for p in ring] land = [project(r) for r in geographic_land] sand = [project(r) for r in beaches] if site == 'gisamun': # Missing OSM beach polygon: approximate only this known beach segment. edge = project([p for p in mainland if config['sandLatitudeMin'] <= p[1] <= config['sandLatitudeMax']]) assert len(edge) >= 2 sand.append(edge + [[x-config['sandWidthMeters'], y] for x,y in reversed(edge)] + [edge[0]]) lines = [project(f['geometry']['coordinates']) for f in coast] def shapes(rings): return [({'type': 'Polygon', 'coordinates': [r]}, 1) for r in rings] def mask(rings, spacing): n = 2 * radius // spacing + 1 return rasterize(shapes(rings), out_shape=(n, n), transform=from_origin(-radius-spacing/2, radius+spacing/2, spacing, spacing), dtype='uint8') land_grid = mask(land, step) beach_grid = mask(sand, step) sources = [ROOT / f'data/raw/hajodae/Copernicus_DSM_COG_10_N{n}_00_E128_00_DEM.tif' for n in ([37,38] if site == 'gisamun' else [38])] heights = np.full((size, size), np.nan, dtype=np.float32) with ExitStack() as stack: datasets = [stack.enter_context(rasterio.open(p)) for p in sources] mosaic, transform = merge(datasets) reproject(mosaic[0], heights, src_transform=transform, src_crs=datasets[0].crs, dst_transform=from_origin(-radius-step/2, radius+step/2, step, step), dst_crs=projection, dst_nodata=np.nan, resampling=Resampling.bilinear) assert np.isfinite(heights).all(), 'Missing DSM coverage' # Preserve the resampled Copernicus heights independently of OSM masks. heights = heights.astype('