Files
sunrise/scripts/index-summit-references.py

88 lines
5.9 KiB
Python

"""Build a local reference index and GeoJSON from the downloaded OSM extract."""
import csv
import hashlib
import html
import json
from pathlib import Path
import xml.etree.ElementTree as ET
from PIL import Image, ImageDraw
root = Path(__file__).resolve().parents[1] / 'data/references/summit'
osm = ET.parse(root / 'maps/summit.osm').getroot()
nodes = {n.attrib['id']: [float(n.attrib['lon']), float(n.attrib['lat'])] for n in osm.findall('node')}
features = []
for element in osm:
tags = {t.attrib['k']: t.attrib['v'] for t in element.findall('tag')}
if not tags or element.tag not in ('node', 'way'):
continue
if element.tag == 'node':
lon,lat = nodes[element.attrib['id']]
if not (126.950 <= lon <= 126.975 and 37.430 <= lat <= 37.453):
continue
geometry = {'type': 'Point', 'coordinates': nodes[element.attrib['id']]}
else:
coordinates = [nodes[n.attrib['ref']] for n in element.findall('nd') if n.attrib['ref'] in nodes]
if len(coordinates) != len(element.findall('nd')) or len(coordinates) < 2:
continue
area = coordinates[0] == coordinates[-1] and (tags.get('area') == 'yes' or 'building' in tags or 'landuse' in tags)
geometry = {'type': 'Polygon' if area else 'LineString', 'coordinates': [coordinates] if area else coordinates}
features.append({'type': 'Feature', 'id': element.tag + '/' + element.attrib['id'], 'properties': tags, 'geometry': geometry})
(root / 'maps/summit.geojson').write_text(json.dumps({'type': 'FeatureCollection', 'features': features}, ensure_ascii=False), encoding='utf-8')
structures = [f for f in features if any(k in f['properties'] for k in ('building', 'man_made', 'barrier')) or f['properties'].get('tourism') == 'viewpoint']
with (root / 'maps/structure-inventory.csv').open('w', encoding='utf-8-sig', newline='') as out:
writer = csv.writer(out)
writer.writerow(['osm_id', 'name', 'tags', 'geometry'])
for f in structures:
writer.writerow([f['id'], f['properties'].get('name', ''), json.dumps(f['properties'], ensure_ascii=False), json.dumps(f['geometry'])])
def xy(c):
return ((c[0]-126.950)/.025*850, (37.453-c[1])/.023*985)
svg = ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 985"><rect width="850" height="985" fill="#eef3ec"/>']
for f in features:
g, p = f['geometry'], f['properties']
if g['type'] == 'Point':
if f not in structures and 'name' not in p:
continue
x,y = xy(g['coordinates'])
svg.append(f'<circle cx="{x}" cy="{y}" r="3" fill="#b94331"/>')
if 'name' in p:
svg.append(f'<text x="{x+5}" y="{y}" font-size="11">{html.escape(p["name"])}</text>')
else:
coords = g['coordinates'][0] if g['type']=='Polygon' else g['coordinates']
points = ' '.join(f'{x:.1f},{y:.1f}' for x,y in map(xy, coords))
building = 'building' in p
svg.append(f'<polyline points="{points}" fill="{"#a5b9cf" if building else "none"}" stroke="{"#345575" if building else "#899b85"}" stroke-width="1.3"/>')
svg.append('<text x="10" y="20" font-size="14">N ↑ | OSM reference map | © OpenStreetMap contributors, ODbL</text></svg>')
(root/'maps/overview.svg').write_text(''.join(svg), encoding='utf-8')
downloads = json.loads((root/'photos/downloads.json').read_text(encoding='utf-8'))
recent_path = root/'photos/recent-downloads.json'
if recent_path.exists():
downloads += json.loads(recent_path.read_text(encoding='utf-8'))
cards=[]
valid=[]
for item in downloads:
if item['status'] != 'downloaded':
continue
if item['file'] in {f'photo-{i:02d}.jpg' for i in range(2,9)} | {'recent-01.jpg','recent-03.jpg','recent-06.jpg'}:
item['relevance'] = '주요 구조물 참고사진에서 제외: 인물, 원경, 계곡 또는 음식'
continue
path=root/'photos'/item['file']
with Image.open(path) as im:
im.verify()
item['sha256']=hashlib.sha256(path.read_bytes()).hexdigest()
valid.append(item)
meta=item['metadata']
details='<br>'.join(html.escape(k)+': '+meta.get(k,{}).get('value','미상') for k in ['Artist','DateTimeOriginal','LicenseShortName','LicenseUrl'])
cards.append(f'<article><h3>{html.escape(item["file"])} — {html.escape(item["title"])}</h3><a href="photos/{item["file"]}"><img src="photos/{item["file"]}"></a><p>{details}</p><a href="{html.escape(item["source"])}">원문 / 이용조건</a></article>')
(root/'photos/verified-index.json').write_text(json.dumps(downloads,ensure_ascii=False,indent=2),encoding='utf-8')
(root/'index.html').write_text('<!doctype html><html lang="ko"><meta charset="utf-8"><title>관악산 정상 구조물 참고자료</title><style>body{font:16px sans-serif;max-width:1100px;margin:30px auto;padding:20px;background:#f5f6f8}img{max-width:100%;max-height:650px}article{background:white;padding:20px;margin:20px 0}a{color:#245f98}</style><h1>관악산 정상 구조물 참고자료</h1><p>수집일 2026-10-29. OSM은 국가 수치지도 대체 참고자료이며 측량도면이 아닙니다. 사진은 촬영 시점의 모습으로 현재 형태와 차이가 있을 수 있습니다. 치수 미확정.</p><p><a href="README.md">자료 범위와 한계</a> · <a href="maps/summit.geojson">GeoJSON</a> · <a href="maps/structure-inventory.csv">구조물 목록</a> · <a href="https://www.openstreetmap.org/copyright">OSM 출처·ODbL</a></p><img src="maps/overview.svg">'+''.join(cards)+'</html>',encoding='utf-8')
sheet=Image.new('RGB',(1000,210*((len(valid)+3)//4)),'white')
draw=ImageDraw.Draw(sheet)
for i,item in enumerate(valid):
with Image.open(root/'photos'/item['file']) as im:
im.thumbnail((245,180))
x,y=(i%4)*250,(i//4)*210
sheet.paste(im,(x,y))
draw.text((x+5,y+183),item['file'],fill='black')
sheet.save(root/'photos/contact-sheet.jpg')
print(json.dumps({'features':len(features),'structure_candidates':len(structures),'verified_photos':len(valid)}))