88 lines
5.9 KiB
Python
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)}))
|