// Draw the generated city footprints over the local satellite atlas, coloured by evidence source, // so a human can see at a glance whether models sit on the imagery instead of on roads or fields. import { readFileSync, writeFileSync } from 'node:fs' import { resolve } from 'node:path' import { PNG } from 'pngjs' import { candidateFootprint } from './oldtown-geometry.mjs' const cityDir = resolve('tools/city-massing/city') const atlas = JSON.parse(readFileSync(resolve(cityDir, 'city-inventory.json'))) const image = PNG.sync.read(readFileSync(resolve(cityDir, 'city-satellite.png'))) const seated = JSON.parse(readFileSync(resolve(cityDir, 'oldtown/seated-plan.json'))) const scale = 2 ** atlas.imagery.zoom * 256 function project([lon, lat]) { const x = ((lon + 180) / 360) * scale - atlas.imagery.westTile * 256 const y = ((1 - Math.asinh(Math.tan((lat * Math.PI) / 180)) / Math.PI) / 2) * scale - atlas.imagery.googleY0 * 256 return [x, y] } const COLORS = { 'imagery-roof-segmentation': [255, 225, 0], 'osm-building-footprint': [0, 235, 255], } function draw(a, b, color, width) { const steps = Math.max(1, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1]))) for (let i = 0; i <= steps; i++) { const x = Math.round(a[0] + ((b[0] - a[0]) * i) / steps) const y = Math.round(a[1] + ((b[1] - a[1]) * i) / steps) for (let dy = -width; dy <= width; dy++) for (let dx = -width; dx <= width; dx++) { const px = x + dx, py = y + dy if (px < 0 || py < 0 || px >= image.width || py >= image.height) continue const k = (py * image.width + px) * 4 image.data[k] = color[0] image.data[k + 1] = color[1] image.data[k + 2] = color[2] image.data[k + 3] = 255 } } } let drawn = 0 const counts = {} for (const building of seated.buildings) { const lon = building.lon ?? building.center?.lon const lat = building.lat ?? building.center?.lat if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue const color = COLORS[building.placementSource] || [255, 0, 0] counts[building.placementSource] = (counts[building.placementSource] || 0) + 1 const ring = candidateFootprint({ lon, lat, width: building.width, depth: building.depth, heading: building.heading, }).geometry.coordinates[0].map(project) for (let i = 0; i < ring.length - 1; i++) draw(ring[i], ring[i + 1], color, 0) drawn++ } const out = resolve(cityDir, 'city-model-coverage.png') writeFileSync(out, PNG.sync.write(image)) // A 1:1 crop of the city core, where the user reported missing and misplaced buildings. const cropSize = 1400 const [cx, cy] = project([110.3965, 28.4570]) const crop = new PNG({ width: cropSize, height: cropSize }) PNG.bitblt(image, crop, Math.round(cx - cropSize / 2), Math.round(cy - cropSize / 2), cropSize, cropSize, 0, 0) const cropOut = resolve(cityDir, 'city-model-coverage-core.png') writeFileSync(cropOut, PNG.sync.write(crop)) console.log(JSON.stringify({ drawn, bySource: counts, full: { path: out, size: [image.width, image.height] }, coreCrop: { path: cropOut, size: [cropSize, cropSize] }, legend: 'yellow = satellite roof segmentation, cyan = mapped OSM footprint', }))