Files
sunrise/scripts/prepare-gwacheon-buildings.py
T

68 lines
3.8 KiB
Python

"""OSM XML snapshots -> attributed footprint GeoJSON, with no fabricated source heights."""
import collections
import argparse
import hashlib
import json
from pathlib import Path
import xml.etree.ElementTree as ET
root = Path(__file__).resolve().parents[1]
items = {}
sources = []
boxes = ['126.975,37.410,126.995,37.435','126.995,37.410,127.015,37.435','126.975,37.435,126.995,37.460','126.995,37.435,127.015,37.460']
for i, bbox in enumerate(boxes):
path = root / f'data/raw/gwacheon/tile-{i}.osm'
tree = ET.parse(path).getroot()
sources.append({'file':path.name,'url':'https://www.openstreetmap.org/api/0.6/map?bbox='+bbox,'sha256':hashlib.sha256(path.read_bytes()).hexdigest()})
for element in tree:
items[(element.tag,element.get('id'))] = element
nodes = {key[1]:[float(e.get('lon')),float(e.get('lat'))] for key,e in items.items() if key[0]=='node'}
def tags(e):
return {t.get('k'):t.get('v') for t in e.findall('tag')}
def ring(e):
refs = [n.get('ref') for n in e.findall('nd')]
if len(refs)<4 or refs[0]!=refs[-1] or any(r not in nodes for r in refs):
return None
return [nodes[r] for r in refs]
features=[]
skipped=[]
consumed=set()
for (kind,identity),e in items.items():
p=tags(e)
if kind!='relation' or 'building' not in p:
continue
outer=[]; inner=[]; members=[]; valid=True
for m in e.findall('member'):
way=items.get(('way',m.get('ref')))
r=ring(way) if way is not None else None
if m.get('type')!='way' or r is None:
valid=False; break
(inner if m.get('role')=='inner' else outer).append(r)
members.append(m.get('ref'))
if valid and len(outer)==1:
features.append({'type':'Feature','id':'relation/'+identity,'properties':p,'geometry':{'type':'Polygon','coordinates':outer+inner}})
consumed.update(members)
else:
skipped.append({'id':'relation/'+identity,'reason':'multipart or incomplete relation; not silently filled'})
for (kind,identity),e in items.items():
p=tags(e)
if kind!='way' or 'building' not in p or identity in consumed:
continue
if p['building'] in ['no','construction','ruins'] or p.get('location')=='underground':
skipped.append({'id':'way/'+identity,'reason':'not an existing above-ground building'}); continue
r=ring(e)
if r is None:
skipped.append({'id':'way/'+identity,'reason':'open or incomplete footprint'}); continue
features.append({'type':'Feature','id':'way/'+identity,'properties':p,'geometry':{'type':'Polygon','coordinates':[r]}})
# API returns ways crossing the requested bounds: retain complete footprints only if center is in AOI.
features=[f for f in features if 126.975 <= sum(c[0] for c in f['geometry']['coordinates'][0][:-1])/len(f['geometry']['coordinates'][0][:-1]) <= 127.015 and 37.410 <= sum(c[1] for c in f['geometry']['coordinates'][0][:-1])/len(f['geometry']['coordinates'][0][:-1]) <= 37.460]
data={'type':'FeatureCollection','attribution':'© OpenStreetMap contributors','license':'ODbL-1.0','features':features}
parser = argparse.ArgumentParser()
parser.add_argument('--output-dir', type=Path, default=root/'public/buildings')
out=parser.parse_args().output_dir
out.mkdir(exist_ok=True,parents=True)
(out/'gwacheon.geojson').write_text(json.dumps(data,ensure_ascii=False,separators=(',',':')),encoding='utf-8')
report={'downloadedAt':'2026-10-29','bbox':[126.975,37.410,127.015,37.460],'count':len(features),'types':dict(collections.Counter(f['properties']['building'] for f in features)),'heightTags':sum('height' in f['properties'] for f in features),'levelTags':sum('building:levels' in f['properties'] for f in features),'sources':sources,'skipped':skipped,'license':'https://www.openstreetmap.org/copyright'}
(out/'metadata.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(report,ensure_ascii=False))