| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 |
- // 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',
- }))
|