68 lines
3.8 KiB
Python
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))
|