mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat(intro): v2 — cities, accelerating sweep, detections, thriller sound design
- City engine: Esri ground, OSM buildings that rise with the scan, road ribbons carrying modelled traffic coloured by congestion, NYC DOT cameras, real ADS-B, MBTA GTFS lines + realtime vehicles, LAX runways with approach strobes, Fort Bragg restricted volumes, Everest terrain with contours. - Indexing keeps accelerating into a 260-capture global sweep while the planet whips a full turn. - The eye opens: aperture blades, sensor array waking, optics calibration, radar sweep, orbiting coordinate ring. - Camera wall with YOLOv8n detections and a city-pulse trace. - Ends on GOD'S EYE VIEW / OMNISCIENCE. - audio/sfx.py: sound design only, built to sit under a music track. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqP857pippaUs6konSDfe8
This commit is contained in:
30 files changed
+8921
-1442
No files matched your search
@@ -0,0 +1,79 @@
|
||||
"""Render city/<slug>/check.png: ground imagery (left) and imagery + OSM overlay (right) for alignment QA.
|
||||
|
||||
usage: python3 -I tools/city/check.py nyc boston [--crop x y size]
|
||||
"""
|
||||
import sys, os, json
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from PIL import Image, ImageDraw
|
||||
from geo import CITIES, OUT, CACHE
|
||||
|
||||
ROADC = [(255, 60, 60), (255, 150, 40), (255, 220, 40), (200, 255, 80), (255, 255, 255), (160, 160, 160)]
|
||||
|
||||
|
||||
def render(slug, S=1400, crop=None):
|
||||
d = os.path.join(OUT, slug)
|
||||
h = CITIES[slug]['half']
|
||||
cx, cy, cs = crop if crop else (0, 0, h) # center and half-size of view in meters
|
||||
g = Image.open(os.path.join(d, 'ground.jpg')).convert('RGB')
|
||||
G = g.size[0]
|
||||
# crop region of ground in pixels
|
||||
l = (cx - cs + h) / (2 * h) * G
|
||||
t = (h - (cy + cs)) / (2 * h) * G
|
||||
r = (cx + cs + h) / (2 * h) * G
|
||||
b = (h - (cy - cs)) / (2 * h) * G
|
||||
base = g.crop((int(l), int(t), int(r), int(b))).resize((S, S), Image.LANCZOS)
|
||||
|
||||
def P(x, y):
|
||||
return ((x - (cx - cs)) / (2 * cs) * S, ((cy + cs) - y) / (2 * cs) * S)
|
||||
|
||||
ov = base.copy()
|
||||
dr = ImageDraw.Draw(ov, 'RGBA')
|
||||
wp = os.path.join(d, 'water.json')
|
||||
if os.path.exists(wp):
|
||||
for poly in json.load(open(wp))['polys']:
|
||||
for k, ring in enumerate(poly):
|
||||
pts = [P(ring[i], ring[i + 1]) for i in range(0, len(ring), 2)]
|
||||
if len(pts) >= 3:
|
||||
dr.polygon(pts, outline=(60, 160, 255, 255), fill=(30, 90, 255, 70) if k == 0 else None)
|
||||
for bld in json.load(open(os.path.join(d, 'buildings.json')))['b']:
|
||||
hgt = bld[0]
|
||||
pts = [P(bld[i], bld[i + 1]) for i in range(2, len(bld), 2)]
|
||||
a = min(255, 60 + int(hgt))
|
||||
col = (0, 255, 200, 255) if hgt >= 100 else (255, 0, 220, 255)
|
||||
dr.polygon(pts, outline=col, fill=(col[0], col[1], col[2], 40 if hgt < 100 else 90))
|
||||
for rd in json.load(open(os.path.join(d, 'roads.json')))['roads']:
|
||||
pts = [P(rd[i], rd[i + 1]) for i in range(3, len(rd), 2)]
|
||||
dr.line(pts, fill=ROADC[rd[0]] + (230,), width=2 if rd[0] < 3 else 1)
|
||||
rp = os.path.join(d, 'rail.json')
|
||||
if os.path.exists(rp):
|
||||
for ln in json.load(open(rp))['lines']:
|
||||
c = ln[0]
|
||||
col = tuple(int(c[i:i + 2], 16) for i in (1, 3, 5)) + (255,)
|
||||
pts = [P(ln[i], ln[i + 1]) for i in range(1, len(ln), 2)]
|
||||
dr.line(pts, fill=col, width=3)
|
||||
ep = os.path.join(d, 'extra.json')
|
||||
if os.path.exists(ep):
|
||||
for x, y, nm in json.load(open(ep))['helipads']:
|
||||
px, py = P(x, y)
|
||||
dr.ellipse((px - 7, py - 7, px + 7, py + 7), outline=(255, 255, 0, 255), width=3)
|
||||
out = Image.new('RGB', (S * 2 + 10, S), (0, 0, 0))
|
||||
out.paste(base, (0, 0))
|
||||
out.paste(ov, (S + 10, 0))
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||
crop = None
|
||||
if '--crop' in sys.argv:
|
||||
i = sys.argv.index('--crop')
|
||||
crop = tuple(float(v) for v in sys.argv[i + 1:i + 4])
|
||||
args = [a for a in args if a not in sys.argv[i + 1:i + 4]]
|
||||
for slug in args or list(CITIES):
|
||||
im = render(slug, crop=crop)
|
||||
if crop:
|
||||
p = os.path.join(CACHE, f'{slug}_check_crop_{int(crop[0])}_{int(crop[1])}_{int(crop[2])}.png')
|
||||
else:
|
||||
p = os.path.join(OUT, slug, 'check.png')
|
||||
im.save(p, optimize=True)
|
||||
print('wrote', p)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Esri World Imagery mosaic -> local ENU square, 4096^2 + 2048^2 JPEG.
|
||||
|
||||
usage: python3 -I tools/city/fetch_imagery.py nyc boston [--zoom 17]
|
||||
"""
|
||||
import sys, os, math, io, time
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image
|
||||
from scipy.ndimage import map_coordinates
|
||||
from geo import CITIES, OUT, CACHE, Frame
|
||||
|
||||
URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
|
||||
TS = 256
|
||||
N = 4096
|
||||
SS = 2 # supersample factor before Lanczos downsample
|
||||
|
||||
|
||||
def merc_px(lat, lon, z):
|
||||
s = TS * 2 ** z
|
||||
x = (lon + 180.0) / 360.0 * s
|
||||
lr = np.radians(lat)
|
||||
y = (1.0 - np.log(np.tan(lr) + 1.0 / np.cos(lr)) / math.pi) / 2.0 * s
|
||||
return x, y
|
||||
|
||||
|
||||
sess = requests.Session()
|
||||
sess.headers['User-Agent'] = 'gods-eye-view-intro/1.0 (film asset build)'
|
||||
|
||||
|
||||
def get_tile(z, x, y):
|
||||
d = os.path.join(CACHE, 'tiles', str(z), str(x))
|
||||
os.makedirs(d, exist_ok=True)
|
||||
p = os.path.join(d, f'{y}.jpg')
|
||||
if os.path.exists(p) and os.path.getsize(p) > 0:
|
||||
return p, 'cache'
|
||||
last = None
|
||||
for attempt in range(5):
|
||||
try:
|
||||
r = sess.get(URL.format(z=z, x=x, y=y), timeout=60)
|
||||
if r.status_code == 200 and r.content[:2] in (b'\xff\xd8', b'\x89P'):
|
||||
with open(p, 'wb') as f:
|
||||
f.write(r.content)
|
||||
return p, 'net'
|
||||
last = f'HTTP {r.status_code} {r.headers.get("content-type")}'
|
||||
except Exception as e: # noqa
|
||||
last = str(e)[:120]
|
||||
time.sleep(1.5 * (attempt + 1))
|
||||
raise RuntimeError(f'tile {z}/{x}/{y} failed: {last}')
|
||||
|
||||
|
||||
def build(slug, z):
|
||||
c = CITIES[slug]
|
||||
F = Frame(**c)
|
||||
h = F.half
|
||||
s, w, n, e = F.bbox(h * 1.02)
|
||||
x0, y0 = merc_px(n, w, z)
|
||||
x1, y1 = merc_px(s, e, z)
|
||||
tx0, ty0 = int(x0 // TS), int(y0 // TS)
|
||||
tx1, ty1 = int(x1 // TS), int(y1 // TS)
|
||||
tiles = [(tx, ty) for ty in range(ty0, ty1 + 1) for tx in range(tx0, tx1 + 1)]
|
||||
print(f'== {slug} z{z}: {tx1-tx0+1}x{ty1-ty0+1} = {len(tiles)} tiles')
|
||||
with ThreadPoolExecutor(8) as ex:
|
||||
res = list(ex.map(lambda t: get_tile(z, t[0], t[1]), tiles))
|
||||
print(f' net {sum(1 for r in res if r[1]=="net")} cache {sum(1 for r in res if r[1]=="cache")}')
|
||||
W, H = (tx1 - tx0 + 1) * TS, (ty1 - ty0 + 1) * TS
|
||||
mos = np.zeros((H, W, 3), np.uint8)
|
||||
for (tx, ty), (p, _) in zip(tiles, res):
|
||||
im = np.asarray(Image.open(p).convert('RGB'))
|
||||
mos[(ty - ty0) * TS:(ty - ty0 + 1) * TS, (tx - tx0) * TS:(tx - tx0 + 1) * TS] = im
|
||||
M = N * SS
|
||||
t = (np.arange(M) + 0.5) / M
|
||||
xs = -h + 2 * h * t # east
|
||||
ys = h - 2 * h * t # north, row 0 at top
|
||||
lon = F.lon0 + xs / F.kx
|
||||
lat = F.lat0 + ys / F.ky
|
||||
px, _ = merc_px(np.full_like(lon, F.lat0), lon, z)
|
||||
_, py = merc_px(lat, np.full_like(lat, F.lon0), z)
|
||||
px = px - tx0 * TS - 0.5 # pixel centers
|
||||
py = py - ty0 * TS - 0.5
|
||||
PX, PY = np.meshgrid(px, py)
|
||||
out = np.empty((M, M, 3), np.uint8)
|
||||
for ch in range(3):
|
||||
out[..., ch] = np.clip(map_coordinates(mos[..., ch].astype(np.float32), [PY, PX], order=1,
|
||||
mode='nearest'), 0, 255).astype(np.uint8)
|
||||
del PX, PY
|
||||
img = Image.fromarray(out)
|
||||
if SS != 1:
|
||||
img = img.resize((N, N), Image.LANCZOS)
|
||||
d = os.path.join(OUT, slug)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
img.save(os.path.join(d, 'ground.jpg'), quality=90, optimize=True)
|
||||
img.resize((2048, 2048), Image.LANCZOS).save(os.path.join(d, 'ground_2k.jpg'), quality=90, optimize=True)
|
||||
img.resize((768, 768), Image.LANCZOS).save(os.path.join(CACHE, f'{slug}_ground_preview.jpg'), quality=85)
|
||||
mpp_src = 156543.03392 * math.cos(math.radians(F.lat0)) / 2 ** z
|
||||
print(f' wrote ground.jpg {os.path.getsize(os.path.join(d, "ground.jpg"))/1e6:.1f}MB; '
|
||||
f'source {mpp_src:.2f} m/px, output {2*h/N:.2f} m/px')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||
z = 17
|
||||
if '--zoom' in sys.argv:
|
||||
z = int(sys.argv[sys.argv.index('--zoom') + 1])
|
||||
for slug in args or list(CITIES):
|
||||
build(slug, z)
|
||||
@@ -0,0 +1,495 @@
|
||||
"""Fetch OSM buildings / roads / water / extras for the city scenes and write city/<slug>/*.json.
|
||||
|
||||
usage: python3 -I tools/city/fetch_osm.py nyc boston
|
||||
"""
|
||||
import sys, os, math, re, json
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from geo import (CITIES, OUT, Frame, overpass, rdp, rdp_ring, area, centroid, open_ring,
|
||||
assemble_rings, clip_polyline, clip_polygon, flat, r1, write_json, dedupe)
|
||||
|
||||
FT = 0.3048
|
||||
|
||||
|
||||
def parse_len(v):
|
||||
"""Parse an OSM length tag to meters. Returns None if unparseable."""
|
||||
if v is None:
|
||||
return None
|
||||
s = str(v).strip().lower().replace(',', '.')
|
||||
s = s.split(';')[0].strip()
|
||||
m = re.match(r"^(\d+(?:\.\d+)?)\s*'\s*(?:(\d+(?:\.\d+)?)\s*\"?)?$", s) # 12'6"
|
||||
if m:
|
||||
return float(m.group(1)) * FT + (float(m.group(2)) * 0.0254 if m.group(2) else 0)
|
||||
m = re.match(r'^(-?\d+(?:\.\d+)?)\s*(m|meters?|metres?|ft|feet|foot)?$', s)
|
||||
if not m:
|
||||
return None
|
||||
val = float(m.group(1))
|
||||
u = m.group(2) or 'm'
|
||||
if u in ('ft', 'feet', 'foot'):
|
||||
val *= FT
|
||||
return val
|
||||
|
||||
|
||||
def parse_num(v):
|
||||
if v is None:
|
||||
return None
|
||||
m = re.match(r'^\s*(\d+(?:\.\d+)?)', str(v).replace(',', '.'))
|
||||
return float(m.group(1)) if m else None
|
||||
|
||||
|
||||
HOUSE = {'house', 'detached', 'semidetached_house', 'terrace', 'bungalow', 'cabin', 'shed', 'garage',
|
||||
'garages', 'hut', 'carport', 'static_caravan'}
|
||||
|
||||
|
||||
def building_height(t):
|
||||
"""Returns (height, min_height, source) where source in h/lv/def."""
|
||||
h = parse_len(t.get('height'))
|
||||
src = 'h'
|
||||
if h is None or h <= 0:
|
||||
lv = parse_num(t.get('building:levels'))
|
||||
if lv is not None and lv > 0:
|
||||
h = lv * 3.2 + (parse_num(t.get('roof:levels')) or 0) * 3.2
|
||||
src = 'lv'
|
||||
else:
|
||||
b = t.get('building', 'yes')
|
||||
h = 7.0 if b in HOUSE else 12.0
|
||||
if b in ('shed', 'garage', 'garages', 'carport', 'hut'):
|
||||
h = 3.5
|
||||
src = 'def'
|
||||
mh = parse_len(t.get('min_height'))
|
||||
if mh is None:
|
||||
ml = parse_num(t.get('building:min_level'))
|
||||
mh = ml * 3.2 if ml else 0.0
|
||||
mh = max(0.0, mh)
|
||||
if mh >= h:
|
||||
mh = 0.0
|
||||
return h, mh, src
|
||||
|
||||
|
||||
def ll_list(geom):
|
||||
return [(g['lat'], g['lon']) for g in geom if g is not None]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ buildings
|
||||
|
||||
def buildings(slug, F):
|
||||
h = F.half
|
||||
s, w, n, e = F.bbox(h + 60)
|
||||
# tile the bbox to keep each response manageable
|
||||
T = 2 if h > 3000 else 1
|
||||
elems = {}
|
||||
for i in range(T):
|
||||
for j in range(T):
|
||||
ts = s + (n - s) * i / T
|
||||
tn = s + (n - s) * (i + 1) / T
|
||||
tw = w + (e - w) * j / T
|
||||
te = w + (e - w) * (j + 1) / T
|
||||
bb = f'{ts:.6f},{tw:.6f},{tn:.6f},{te:.6f}'
|
||||
q = (f'[out:json][timeout:180];(way["building"]({bb});'
|
||||
f'relation["building"]["type"="multipolygon"]({bb}););out geom;')
|
||||
print(f' buildings tile {i},{j}')
|
||||
d = overpass(q, f'{slug}_bld_{i}{j}')
|
||||
for el in d['elements']:
|
||||
elems[(el['type'], el['id'])] = el
|
||||
print(f' {len(elems)} building elements')
|
||||
out = []
|
||||
stats = dict(h=0, lv=0, df=0, dropped_small=0, dropped_out=0, rel_rings=0)
|
||||
for (typ, _id), el in elems.items():
|
||||
t = el.get('tags', {})
|
||||
if t.get('building') in ('no', 'construction_site'):
|
||||
continue
|
||||
rings = []
|
||||
if typ == 'way':
|
||||
if 'geometry' not in el:
|
||||
continue
|
||||
g = ll_list(el['geometry'])
|
||||
if len(g) >= 4 and g[0] == g[-1]:
|
||||
rings.append(g)
|
||||
else:
|
||||
outers = [ll_list(m['geometry']) for m in el.get('members', [])
|
||||
if m['type'] == 'way' and m.get('role') == 'outer' and 'geometry' in m]
|
||||
rings = assemble_rings(outers)
|
||||
stats['rel_rings'] += len(rings)
|
||||
hgt, mh, src = building_height(t)
|
||||
for g in rings:
|
||||
pts = open_ring([F.xy(la, lo) for la, lo in g])
|
||||
pts = rdp_ring(pts, 0.5)
|
||||
if len(pts) < 3 or abs(area(pts)) < 8:
|
||||
stats['dropped_small'] += 1
|
||||
continue
|
||||
cx, cy = centroid(pts)
|
||||
if abs(cx) > h or abs(cy) > h:
|
||||
stats['dropped_out'] += 1
|
||||
continue
|
||||
stats[{'h': 'h', 'lv': 'lv', 'def': 'df'}[src]] += 1
|
||||
out.append((cx * cx + cy * cy, [r1(hgt), r1(mh)] + flat(pts)))
|
||||
out.sort(key=lambda a: a[0])
|
||||
b = [o[1] for o in out]
|
||||
write_json(os.path.join(OUT, slug, 'buildings.json'), {'count': len(b), 'b': b})
|
||||
hs = sorted(x[0] for x in b)
|
||||
print(f' buildings {len(b)} height-src tag={stats["h"]} levels={stats["lv"]} default={stats["df"]} '
|
||||
f'dropped small={stats["dropped_small"]} outside={stats["dropped_out"]} tallest={hs[-5:]}')
|
||||
return dict(count=len(b), height_from_tag=stats['h'], height_from_levels=stats['lv'],
|
||||
height_default=stats['df'], tallest_m=hs[-1] if hs else None)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ roads
|
||||
|
||||
CLS = {'motorway': 0, 'trunk': 0, 'primary': 1, 'secondary': 2, 'tertiary': 3,
|
||||
'residential': 4, 'unclassified': 4, 'service': 5}
|
||||
DEF_LANES = {0: 3, 1: 2, 2: 2, 3: 2, 4: 1, 5: 1}
|
||||
|
||||
|
||||
def plen(pts):
|
||||
return sum(math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) for i in range(len(pts) - 1))
|
||||
|
||||
|
||||
def roads(slug, F):
|
||||
h = F.half
|
||||
s, w, n, e = F.bbox(h + 100)
|
||||
bb = f'{s:.6f},{w:.6f},{n:.6f},{e:.6f}'
|
||||
q = (f'[out:json][timeout:180];way["highway"~"^(motorway|trunk|primary|secondary|tertiary|residential|'
|
||||
f'unclassified|service|motorway_link|trunk_link|primary_link|secondary_link|tertiary_link)$"]({bb});out geom;')
|
||||
print(' roads')
|
||||
d = overpass(q, f'{slug}_roads')
|
||||
out = []
|
||||
cnt = [0] * 6
|
||||
tunnels = 0
|
||||
short_service = 0
|
||||
for el in d['elements']:
|
||||
t = el.get('tags', {})
|
||||
hw = t['highway']
|
||||
if t.get('area') == 'yes':
|
||||
continue
|
||||
if t.get('tunnel') in ('yes', 'covered') or t.get('layer', '0').startswith('-') and t.get('tunnel'):
|
||||
tunnels += 1
|
||||
continue
|
||||
pts = [F.xy(la, lo) for la, lo in ll_list(el.get('geometry', []))]
|
||||
pts = dedupe(pts)
|
||||
if len(pts) < 2:
|
||||
continue
|
||||
if hw.endswith('_link'):
|
||||
c = 5
|
||||
else:
|
||||
c = CLS[hw]
|
||||
if hw == 'service' and plen(pts) <= 80:
|
||||
short_service += 1
|
||||
continue
|
||||
ow = t.get('oneway', '')
|
||||
oneway = 0
|
||||
if ow in ('yes', '1', 'true'):
|
||||
oneway = 1
|
||||
elif ow == '-1':
|
||||
oneway = 1
|
||||
pts = pts[::-1]
|
||||
elif ow == 'no':
|
||||
oneway = 0
|
||||
elif hw in ('motorway', 'motorway_link') or t.get('junction') in ('roundabout', 'circular'):
|
||||
oneway = 1
|
||||
ln = parse_num(t.get('lanes'))
|
||||
lanes = int(ln) if ln else DEF_LANES[c]
|
||||
for piece in clip_polyline(pts, -h, -h, h, h):
|
||||
piece = rdp(piece, 1.0)
|
||||
if len(piece) < 2 or plen(piece) < 1:
|
||||
continue
|
||||
out.append([c, oneway, lanes] + flat(piece))
|
||||
cnt[c] += 1
|
||||
write_json(os.path.join(OUT, slug, 'roads.json'), {'roads': out})
|
||||
print(f' roads {len(out)} by class {cnt} (dropped tunnels {tunnels}, short service {short_service})')
|
||||
return dict(count=len(out), by_class=cnt, dropped_tunnel_ways=tunnels, dropped_short_service=short_service)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ water
|
||||
|
||||
def coast_water(slug, F, h):
|
||||
"""Build water polygons inside the square from natural=coastline (land left, water right)."""
|
||||
s, w, n, e = F.bbox(h * 1.6)
|
||||
bb = f'{s:.6f},{w:.6f},{n:.6f},{e:.6f}'
|
||||
q = f'[out:json][timeout:180];way["natural"="coastline"]({bb});out geom;'
|
||||
print(' coastline')
|
||||
d = overpass(q, f'{slug}_coast')
|
||||
ways = [[F.xy(la, lo) for la, lo in ll_list(el['geometry'])] for el in d['elements'] if 'geometry' in el]
|
||||
# directional merge into chains
|
||||
chains = []
|
||||
pool = [wy for wy in ways if len(wy) >= 2]
|
||||
while pool:
|
||||
cur = pool.pop()
|
||||
grown = True
|
||||
while grown and cur[0] != cur[-1]:
|
||||
grown = False
|
||||
for i, wy in enumerate(pool):
|
||||
if wy[0] == cur[-1]:
|
||||
cur = cur + wy[1:]
|
||||
elif wy[-1] == cur[0]:
|
||||
cur = wy + cur[1:]
|
||||
else:
|
||||
continue
|
||||
pool.pop(i)
|
||||
grown = True
|
||||
break
|
||||
chains.append(cur)
|
||||
eps = 1e-6
|
||||
|
||||
def on_edge(p):
|
||||
return abs(abs(p[0]) - h) < 1e-3 or abs(abs(p[1]) - h) < 1e-3
|
||||
|
||||
def spos(p): # clockwise perimeter coordinate from top-left
|
||||
x, y = p
|
||||
if abs(y - h) < 1e-3:
|
||||
return x + h
|
||||
if abs(x - h) < 1e-3:
|
||||
return 2 * h + (h - y)
|
||||
if abs(y + h) < 1e-3:
|
||||
return 4 * h + (h - x)
|
||||
return 6 * h + (y + h)
|
||||
|
||||
corners = [(0.0, (-h, h)), (2 * h, (h, h)), (4 * h, (h, -h)), (6 * h, (-h, -h))]
|
||||
pieces, islands, bad = [], [], 0
|
||||
for ch in chains:
|
||||
closed = ch[0] == ch[-1]
|
||||
inside_all = all(abs(x) <= h and abs(y) <= h for x, y in ch)
|
||||
if closed and inside_all:
|
||||
islands.append(open_ring(ch))
|
||||
continue
|
||||
for pc in clip_polyline(ch, -h, -h, h, h):
|
||||
if on_edge(pc[0]) and on_edge(pc[-1]):
|
||||
pieces.append(pc)
|
||||
else:
|
||||
bad += 1
|
||||
polys = []
|
||||
if pieces:
|
||||
used = [False] * len(pieces)
|
||||
starts = [spos(p[0]) for p in pieces]
|
||||
for k in range(len(pieces)):
|
||||
if used[k]:
|
||||
continue
|
||||
ring = []
|
||||
cur = k
|
||||
guard = 0
|
||||
while True:
|
||||
used[cur] = True
|
||||
ring += pieces[cur]
|
||||
se = spos(pieces[cur][-1])
|
||||
# next entry clockwise from se
|
||||
best, bd = None, None
|
||||
for m in range(len(pieces)):
|
||||
dd = (starts[m] - se) % (8 * h)
|
||||
if bd is None or dd < bd:
|
||||
best, bd = m, dd
|
||||
# add corners passed between se and se+bd
|
||||
for cs, cp in sorted(corners, key=lambda c: (c[0] - se) % (8 * h)):
|
||||
if 0 < (cs - se) % (8 * h) < bd:
|
||||
ring.append(cp)
|
||||
cur = best
|
||||
guard += 1
|
||||
if cur == k or guard > len(pieces) + 2:
|
||||
break
|
||||
if used[cur]:
|
||||
break
|
||||
polys.append([dedupe(open_ring(ring))])
|
||||
# attach islands (land holes) to the containing water polygon
|
||||
def pip(pt, ring):
|
||||
x, y = pt
|
||||
c = False
|
||||
for i in range(len(ring)):
|
||||
x1, y1 = ring[i]
|
||||
x2, y2 = ring[i - 1]
|
||||
if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1:
|
||||
c = not c
|
||||
return c
|
||||
for isl in islands:
|
||||
if abs(area(isl)) < 50:
|
||||
continue
|
||||
for p in polys:
|
||||
if pip(isl[0], p[0]):
|
||||
p.append(isl)
|
||||
break
|
||||
print(f' coastline chains {len(chains)} pieces {len(pieces)} islands {len(islands)} '
|
||||
f'water polys {len(polys)} (unclosed pieces skipped {bad})')
|
||||
return polys, bad
|
||||
|
||||
|
||||
def water(slug, F):
|
||||
h = F.half
|
||||
s, w, n, e = F.bbox(h + 50)
|
||||
bb = f'{s:.6f},{w:.6f},{n:.6f},{e:.6f}'
|
||||
q = (f'[out:json][timeout:180];(way["natural"="water"]({bb});relation["natural"="water"]({bb});'
|
||||
f'way["waterway"="riverbank"]({bb});relation["waterway"="riverbank"]({bb}););out geom;')
|
||||
print(' water areas')
|
||||
d = overpass(q, f'{slug}_water')
|
||||
polys = []
|
||||
for el in d['elements']:
|
||||
t = el.get('tags', {})
|
||||
if t.get('water') in ('pond', 'reflecting_pool', 'fountain', 'basin', 'reservoir') and el['type'] == 'way':
|
||||
pass # keep small ponds too; they are real water
|
||||
if el['type'] == 'way':
|
||||
if 'geometry' not in el:
|
||||
continue
|
||||
g = ll_list(el['geometry'])
|
||||
if g[0] != g[-1]:
|
||||
continue
|
||||
outers, inners = [g], []
|
||||
else:
|
||||
mem = [m for m in el.get('members', []) if m['type'] == 'way' and 'geometry' in m]
|
||||
outers = assemble_rings([ll_list(m['geometry']) for m in mem if m.get('role') == 'outer'])
|
||||
inners = assemble_rings([ll_list(m['geometry']) for m in mem if m.get('role') == 'inner'])
|
||||
inn_xy = [open_ring([F.xy(*p) for p in r]) for r in inners]
|
||||
for o in outers:
|
||||
ring = open_ring([F.xy(*p) for p in o])
|
||||
ring = clip_polygon(ring, -h, -h, h, h)
|
||||
ring = rdp_ring(open_ring(ring), 1.0)
|
||||
if len(ring) < 3 or abs(area(ring)) < 20:
|
||||
continue
|
||||
p = [ring]
|
||||
for ir in inn_xy:
|
||||
ir2 = rdp_ring(open_ring(clip_polygon(ir, -h, -h, h, h)), 1.0)
|
||||
if len(ir2) >= 3 and abs(area(ir2)) >= 20:
|
||||
p.append(ir2)
|
||||
polys.append(('water', p))
|
||||
n_area = len(polys)
|
||||
cpolys, bad = coast_water(slug, F, h)
|
||||
for p in cpolys:
|
||||
p2 = [rdp_ring(r, 1.0) for r in p]
|
||||
p2 = [r for r in p2 if len(r) >= 3]
|
||||
if p2:
|
||||
polys.append(('coast', p2))
|
||||
out = [[flat(r) for r in p] for _, p in polys]
|
||||
src = [s_ for s_, _ in polys]
|
||||
write_json(os.path.join(OUT, slug, 'water.json'),
|
||||
{'count': len(out), 'format': 'polys[i] = [outerRing, holeRing...], rings flat x,y', 'src': src,
|
||||
'polys': out})
|
||||
print(f' water polys {len(out)} (osm water areas {n_area}, coastline-derived {len(cpolys)})')
|
||||
return dict(count=len(out), water_areas=n_area, coastline_polys=len(cpolys), coastline_unclosed_pieces=bad)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ extras
|
||||
|
||||
KNOWN_HELI = ['Downtown Manhattan Heliport', 'West 30th Street Heliport', 'East 34th Street Heliport']
|
||||
|
||||
|
||||
def nyc_extra(slug, F):
|
||||
s, w, n, e = F.bbox(F.half + 2000)
|
||||
bb = f'{s:.6f},{w:.6f},{n:.6f},{e:.6f}'
|
||||
q = (f'[out:json][timeout:180];(nwr["aeroway"~"^(helipad|heliport)$"]({bb});'
|
||||
f'nwr["name"~"heliport|helistop|helipad",i]({bb}););out center tags;')
|
||||
print(' helipads')
|
||||
d = overpass(q, f'{slug}_heli2')
|
||||
pads = []
|
||||
for el in d['elements']:
|
||||
t = el.get('tags', {})
|
||||
if el['type'] == 'node':
|
||||
la, lo = el['lat'], el['lon']
|
||||
else:
|
||||
c = el.get('center')
|
||||
if not c:
|
||||
continue
|
||||
la, lo = c['lat'], c['lon']
|
||||
x, y = F.xy(la, lo)
|
||||
name = t.get('name') or t.get('operator') or ''
|
||||
kind = t.get('aeroway') or ('heliport' if 'heliport' in name.lower() else 'named')
|
||||
pads.append(dict(x=r1(x), y=r1(y), name=name, kind=kind, ref=t.get('ref', ''), osm=f"{el['type']}/{el['id']}",
|
||||
tags={k: v for k, v in t.items() if k in ('aeroway', 'faa', 'operator', 'man_made', 'amenity')},
|
||||
inside=abs(x) <= F.half and abs(y) <= F.half))
|
||||
pads.sort(key=lambda p: (p['kind'] != 'heliport', p['x'] ** 2 + p['y'] ** 2))
|
||||
for p in pads:
|
||||
print(f" {p['kind']:9s} {p['x']:8.1f} {p['y']:8.1f} {'in ' if p['inside'] else 'OUT'} {p['name']}")
|
||||
found = {k: [p for p in pads if p['kind'] == 'heliport' and k.split()[0].lower() in p['name'].lower()
|
||||
and 'heliport' in p['name'].lower()] for k in KNOWN_HELI}
|
||||
helipads = [[p['x'], p['y'], p['name'] or p['kind']] for p in pads]
|
||||
write_json(os.path.join(OUT, slug, 'extra.json'),
|
||||
{'helipads': helipads,
|
||||
'detail': pads,
|
||||
'note': 'aeroway=helipad|heliport from OSM in a bbox 2 km wider than the scene; '
|
||||
'detail[].inside says whether the point is inside the scene square'})
|
||||
return dict(helipad_points=len(pads), named_heliports={k: (v[0]['x'], v[0]['y'], v[0]['inside']) if v else None
|
||||
for k, v in found.items()})
|
||||
|
||||
|
||||
def boston_rail(slug, F):
|
||||
h = F.half
|
||||
s, w, n, e = F.bbox(h + 100)
|
||||
bb = f'{s:.6f},{w:.6f},{n:.6f},{e:.6f}'
|
||||
q = (f'[out:json][timeout:180];(relation["route"~"^(subway|light_rail|train|tram)$"]({bb});'
|
||||
f'way["railway"~"^(subway|light_rail|rail|tram)$"]({bb}););out geom;')
|
||||
print(' rail')
|
||||
d = overpass(q, f'{slug}_rail')
|
||||
ways = {el['id']: el for el in d['elements'] if el['type'] == 'way'}
|
||||
rels = [el for el in d['elements'] if el['type'] == 'relation']
|
||||
lines, seen = [], set()
|
||||
rel_names = []
|
||||
for r in rels:
|
||||
t = r.get('tags', {})
|
||||
col = (t.get('colour') or t.get('color') or '').strip()
|
||||
if not re.match(r'^#?[0-9a-fA-F]{6}$', col):
|
||||
col = {'subway': '#888888', 'light_rail': '#00843D', 'tram': '#00843D', 'train': '#80276C'}[t['route']]
|
||||
if not col.startswith('#'):
|
||||
col = '#' + col
|
||||
col = col.upper()
|
||||
rel_names.append(f"{t.get('route')}:{t.get('name', t.get('ref', ''))}:{col}")
|
||||
for m in r.get('members', []):
|
||||
if m['type'] != 'way' or 'geometry' not in m or m.get('role') in ('platform', 'stop'):
|
||||
continue
|
||||
wt = ways.get(m['ref'], {}).get('tags', {})
|
||||
if wt.get('public_transport') or wt.get('railway') == 'platform':
|
||||
continue
|
||||
key = (m['ref'], col)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
pts = [F.xy(la, lo) for la, lo in ll_list(m['geometry'])]
|
||||
for pc in clip_polyline(dedupe(pts), -h, -h, h, h):
|
||||
pc = rdp(pc, 1.0)
|
||||
if len(pc) >= 2:
|
||||
lines.append([col] + flat(pc))
|
||||
covered = {k[0] for k in seen}
|
||||
extra = 0
|
||||
for wid, el in ways.items():
|
||||
t = el.get('tags', {})
|
||||
if wid in covered or t.get('service') or 'geometry' not in el:
|
||||
continue
|
||||
pts = [F.xy(la, lo) for la, lo in ll_list(el['geometry'])]
|
||||
for pc in clip_polyline(dedupe(pts), -h, -h, h, h):
|
||||
pc = rdp(pc, 1.0)
|
||||
if len(pc) >= 2:
|
||||
lines.append(['#7F7F7F'] + flat(pc))
|
||||
extra += 1
|
||||
write_json(os.path.join(OUT, slug, 'rail.json'),
|
||||
{'lines': lines, 'routes': sorted(set(rel_names)),
|
||||
'note': 'OSM route relations (subway/light_rail/train/tram) colour tags; #7F7F7F = railway way not in any route relation'})
|
||||
print(f' rail lines {len(lines)} from {len(rels)} route relations (+{extra} unassigned way pieces)')
|
||||
for nm in sorted(set(rel_names)):
|
||||
print(' ', nm)
|
||||
return dict(polylines=len(lines), route_relations=len(rels), unassigned_pieces=extra)
|
||||
|
||||
|
||||
def main():
|
||||
slugs = sys.argv[1:] or list(CITIES)
|
||||
report = {}
|
||||
for slug in slugs:
|
||||
c = CITIES[slug]
|
||||
F = Frame(**c)
|
||||
print(f'== {slug}')
|
||||
write_json(os.path.join(OUT, slug, 'meta.json'),
|
||||
{'slug': slug, 'name': c['name'], 'lat0': c['lat0'], 'lon0': c['lon0'], 'half': c['half'],
|
||||
'frame': 'ENU m',
|
||||
'projection': 'x=(lon-lon0)*cos(lat0)*111320, y=(lat-lat0)*110540',
|
||||
'source': 'OpenStreetMap contributors (ODbL) via Overpass; imagery Esri World Imagery'})
|
||||
r = {}
|
||||
r['buildings'] = buildings(slug, F)
|
||||
r['roads'] = roads(slug, F)
|
||||
try:
|
||||
r['water'] = water(slug, F)
|
||||
except Exception as ex: # noqa
|
||||
print(' WATER FAILED', ex)
|
||||
r['water'] = f'failed: {ex}'
|
||||
if slug == 'nyc':
|
||||
r['extra'] = nyc_extra(slug, F)
|
||||
if slug == 'boston':
|
||||
r['rail'] = boston_rail(slug, F)
|
||||
report[slug] = r
|
||||
print(json.dumps(report, indent=1))
|
||||
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cache', 'osm_report.json'), 'w') as f:
|
||||
json.dump(report, f, indent=1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,288 @@
|
||||
"""Shared helpers for the city scene geodata fetchers (ENU frame, Overpass, geometry)."""
|
||||
import json, math, os, time, hashlib
|
||||
import requests
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
|
||||
OUT = os.path.join(ROOT, 'city')
|
||||
CACHE = os.path.join(HERE, 'cache')
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
|
||||
CITIES = {
|
||||
'nyc': dict(name='New York — Midtown / Chelsea / Lower Manhattan', lat0=40.7400, lon0=-73.9900, half=3500),
|
||||
'boston': dict(name='Boston — Downtown / Back Bay / Cambridge / Seaport', lat0=42.3555, lon0=-71.0605, half=3200),
|
||||
}
|
||||
|
||||
OVERPASS = [
|
||||
'https://overpass-api.de/api/interpreter',
|
||||
'https://overpass.kumi.systems/api/interpreter',
|
||||
'https://overpass.private.coffee/api/interpreter',
|
||||
'https://maps.mail.ru/osm/tools/overpass/api/interpreter',
|
||||
]
|
||||
|
||||
|
||||
class Frame:
|
||||
def __init__(self, lat0, lon0, half, **_):
|
||||
self.lat0, self.lon0, self.half = lat0, lon0, half
|
||||
self.kx = math.cos(math.radians(lat0)) * 111320.0
|
||||
self.ky = 110540.0
|
||||
|
||||
def xy(self, lat, lon):
|
||||
return ((lon - self.lon0) * self.kx, (lat - self.lat0) * self.ky)
|
||||
|
||||
def ll(self, x, y):
|
||||
return (self.lat0 + y / self.ky, self.lon0 + x / self.kx)
|
||||
|
||||
def bbox(self, half=None):
|
||||
h = self.half if half is None else half
|
||||
s, w = self.ll(-h, -h)
|
||||
n, e = self.ll(h, h)
|
||||
return s, w, n, e
|
||||
|
||||
|
||||
def overpass(query, tag):
|
||||
"""Run an Overpass query, caching the raw JSON response under cache/osm_<tag>.json."""
|
||||
path = os.path.join(CACHE, f'osm_{tag}.json')
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
last = None
|
||||
for attempt in range(8):
|
||||
for url in OVERPASS:
|
||||
try:
|
||||
t = time.time()
|
||||
r = requests.post(url, data={'data': query}, timeout=400)
|
||||
if r.status_code != 200:
|
||||
last = f'{url} HTTP {r.status_code}'
|
||||
print(' overpass fail', last)
|
||||
continue
|
||||
d = r.json()
|
||||
if 'remark' in d and 'error' in d.get('remark', '').lower():
|
||||
last = f'{url} remark {d["remark"][:200]}'
|
||||
print(' overpass remark', last)
|
||||
continue
|
||||
print(f' overpass ok {url} {len(r.content)/1e6:.1f}MB {time.time()-t:.0f}s {len(d["elements"])} el')
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f)
|
||||
return d
|
||||
except Exception as e: # noqa
|
||||
last = f'{url} {type(e).__name__}: {str(e)[:200]}'
|
||||
print(' overpass fail', last)
|
||||
time.sleep(15 + 15 * attempt)
|
||||
raise RuntimeError('all overpass endpoints failed: ' + str(last))
|
||||
|
||||
|
||||
# ---------------- geometry ----------------
|
||||
|
||||
def rdp(pts, eps):
|
||||
"""Douglas-Peucker on an open polyline (list of (x,y))."""
|
||||
n = len(pts)
|
||||
if n < 3:
|
||||
return list(pts)
|
||||
keep = [False] * n
|
||||
keep[0] = keep[-1] = True
|
||||
stack = [(0, n - 1)]
|
||||
while stack:
|
||||
a, b = stack.pop()
|
||||
ax, ay = pts[a]
|
||||
bx, by = pts[b]
|
||||
dx, dy = bx - ax, by - ay
|
||||
L = math.hypot(dx, dy)
|
||||
dmax, idx = -1.0, -1
|
||||
for i in range(a + 1, b):
|
||||
px, py = pts[i]
|
||||
if L < 1e-9:
|
||||
d = math.hypot(px - ax, py - ay)
|
||||
else:
|
||||
d = abs(dx * (ay - py) - dy * (ax - px)) / L
|
||||
if d > dmax:
|
||||
dmax, idx = d, i
|
||||
if dmax > eps and idx > 0:
|
||||
keep[idx] = True
|
||||
stack.append((a, idx))
|
||||
stack.append((idx, b))
|
||||
return [p for p, k in zip(pts, keep) if k]
|
||||
|
||||
|
||||
def rdp_ring(ring, eps):
|
||||
"""Douglas-Peucker for a closed ring given without the repeated closing vertex."""
|
||||
n = len(ring)
|
||||
if n < 4:
|
||||
return list(ring)
|
||||
# split at vertex 0 and the vertex farthest from it
|
||||
x0, y0 = ring[0]
|
||||
far = max(range(n), key=lambda i: (ring[i][0] - x0) ** 2 + (ring[i][1] - y0) ** 2)
|
||||
a = rdp(ring[0:far + 1], eps)
|
||||
b = rdp(ring[far:] + [ring[0]], eps)
|
||||
return a[:-1] + b[:-1]
|
||||
|
||||
|
||||
def area(ring):
|
||||
s = 0.0
|
||||
n = len(ring)
|
||||
for i in range(n):
|
||||
x1, y1 = ring[i]
|
||||
x2, y2 = ring[(i + 1) % n]
|
||||
s += x1 * y2 - x2 * y1
|
||||
return s / 2.0
|
||||
|
||||
|
||||
def centroid(ring):
|
||||
a = area(ring)
|
||||
if abs(a) < 1e-9:
|
||||
xs = [p[0] for p in ring]
|
||||
ys = [p[1] for p in ring]
|
||||
return sum(xs) / len(xs), sum(ys) / len(ys)
|
||||
cx = cy = 0.0
|
||||
n = len(ring)
|
||||
for i in range(n):
|
||||
x1, y1 = ring[i]
|
||||
x2, y2 = ring[(i + 1) % n]
|
||||
c = x1 * y2 - x2 * y1
|
||||
cx += (x1 + x2) * c
|
||||
cy += (y1 + y2) * c
|
||||
return cx / (6 * a), cy / (6 * a)
|
||||
|
||||
|
||||
def dedupe(pts):
|
||||
out = []
|
||||
for p in pts:
|
||||
if not out or p != out[-1]:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def open_ring(pts):
|
||||
pts = dedupe(pts)
|
||||
if len(pts) > 1 and pts[0] == pts[-1]:
|
||||
pts = pts[:-1]
|
||||
return pts
|
||||
|
||||
|
||||
def assemble_rings(ways):
|
||||
"""Join way coordinate lists (lists of (lat,lon) tuples) into closed rings by matching endpoints."""
|
||||
rings = []
|
||||
pool = [list(w) for w in ways if len(w) >= 2]
|
||||
while pool:
|
||||
cur = pool.pop()
|
||||
changed = True
|
||||
while cur[0] != cur[-1] and changed:
|
||||
changed = False
|
||||
for i, w in enumerate(pool):
|
||||
if w[0] == cur[-1]:
|
||||
cur = cur + w[1:]
|
||||
elif w[-1] == cur[-1]:
|
||||
cur = cur + w[::-1][1:]
|
||||
elif w[-1] == cur[0]:
|
||||
cur = w + cur[1:]
|
||||
elif w[0] == cur[0]:
|
||||
cur = w[::-1] + cur[1:]
|
||||
else:
|
||||
continue
|
||||
pool.pop(i)
|
||||
changed = True
|
||||
break
|
||||
if cur[0] == cur[-1] and len(cur) >= 4:
|
||||
rings.append(cur)
|
||||
return rings
|
||||
|
||||
|
||||
def clip_polyline(pts, xmin, ymin, xmax, ymax):
|
||||
"""Clip a polyline to a rectangle (Liang-Barsky per segment); returns list of pieces."""
|
||||
pieces, cur = [], []
|
||||
|
||||
def inside(p):
|
||||
return xmin <= p[0] <= xmax and ymin <= p[1] <= ymax
|
||||
|
||||
for i in range(len(pts) - 1):
|
||||
(x1, y1), (x2, y2) = pts[i], pts[i + 1]
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
t0, t1 = 0.0, 1.0
|
||||
ok = True
|
||||
for p, q in ((-dx, x1 - xmin), (dx, xmax - x1), (-dy, y1 - ymin), (dy, ymax - y1)):
|
||||
if p == 0:
|
||||
if q < 0:
|
||||
ok = False
|
||||
break
|
||||
else:
|
||||
t = q / p
|
||||
if p < 0:
|
||||
if t > t1:
|
||||
ok = False
|
||||
break
|
||||
t0 = max(t0, t)
|
||||
else:
|
||||
if t < t0:
|
||||
ok = False
|
||||
break
|
||||
t1 = min(t1, t)
|
||||
if not ok:
|
||||
if cur:
|
||||
pieces.append(cur)
|
||||
cur = []
|
||||
continue
|
||||
a = (x1 + t0 * dx, y1 + t0 * dy)
|
||||
b = (x1 + t1 * dx, y1 + t1 * dy)
|
||||
if not cur:
|
||||
cur = [a, b]
|
||||
else:
|
||||
if cur[-1] != a:
|
||||
pieces.append(cur)
|
||||
cur = [a, b]
|
||||
else:
|
||||
cur.append(b)
|
||||
if t1 < 1.0:
|
||||
pieces.append(cur)
|
||||
cur = []
|
||||
if cur:
|
||||
pieces.append(cur)
|
||||
return [dedupe(p) for p in pieces if len(dedupe(p)) >= 2]
|
||||
|
||||
|
||||
def clip_polygon(ring, xmin, ymin, xmax, ymax):
|
||||
"""Sutherland-Hodgman clip of a ring to a rectangle."""
|
||||
def clip(poly, inside, inter):
|
||||
out = []
|
||||
n = len(poly)
|
||||
for i in range(n):
|
||||
cur, prev = poly[i], poly[i - 1]
|
||||
if inside(cur):
|
||||
if not inside(prev):
|
||||
out.append(inter(prev, cur))
|
||||
out.append(cur)
|
||||
elif inside(prev):
|
||||
out.append(inter(prev, cur))
|
||||
return out
|
||||
|
||||
def ix(xc):
|
||||
return lambda p, q: (xc, p[1] + (q[1] - p[1]) * (xc - p[0]) / (q[0] - p[0]))
|
||||
|
||||
def iy(yc):
|
||||
return lambda p, q: (p[0] + (q[0] - p[0]) * (yc - p[1]) / (q[1] - p[1]), yc)
|
||||
|
||||
poly = list(ring)
|
||||
for ins, inter in ((lambda p: p[0] >= xmin, ix(xmin)), (lambda p: p[0] <= xmax, ix(xmax)),
|
||||
(lambda p: p[1] >= ymin, iy(ymin)), (lambda p: p[1] <= ymax, iy(ymax))):
|
||||
if not poly:
|
||||
break
|
||||
poly = clip(poly, ins, inter)
|
||||
return dedupe(poly)
|
||||
|
||||
|
||||
def r1(v):
|
||||
return round(v, 1)
|
||||
|
||||
|
||||
def flat(pts):
|
||||
out = []
|
||||
for x, y in pts:
|
||||
out.append(r1(x))
|
||||
out.append(r1(y))
|
||||
return out
|
||||
|
||||
|
||||
def write_json(path, obj):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w') as f:
|
||||
json.dump(obj, f, separators=(',', ':'))
|
||||
print(f' wrote {os.path.relpath(path, ROOT)} {os.path.getsize(path)/1e6:.2f}MB')
|
||||
Reference in new issue
Block a user