| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 |
- // Offline survey/active-model gap audit. A missing ID is not automatically a missing building.
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
- import { resolve, join } from 'node:path'
- import { createHash } from 'node:crypto'
- import * as turf from '@turf/turf'
- const root = resolve('tools/city-massing/city/cad-bim')
- const out = resolve(process.argv[2] || '.scratch/cockpit-shell-panel-layout/verify/artifacts/t35/large-building-audit')
- mkdirSync(out, { recursive: true })
- const read = (file) => JSON.parse(readFileSync(file, 'utf8'))
- const plan = read(join(root, 'plan.json')), seated = read(join(root, 'seated-plan.json'))
- const survey = read(resolve('tools/city-massing/city/cad-buildings.geojson')).features
- const names = ['pilot', 'batch-urban8000-dedup', 'batch-urban8000b', 'batch-urban8000c',
- 'batch-urban-final', 'batch-urban-tail', 'batch-unfitted-fallback'].map((name) => `lod-${name}-roofed2`)
- const active = new Set(names.flatMap((name) => read(join(root, name, 'manifest.json')).cells.flatMap((c) => c.buildingIds)))
- const seatedById = new Map(seated.buildings.map((b) => [b.id, b]))
- const excluded = new Map(seated.excluded.map((b) => [b.id, b]))
- const planned = new Map(plan.buildings.map((b) => [b.id, b]))
- const featureFor = (id) => survey[Number(id.slice(4))]
- const grid = new Map(), gridSize = 80
- const bboxENU = (b) => [Math.min(...b.footprintENU.map((p) => p[0])), Math.min(...b.footprintENU.map((p) => p[1])),
- Math.max(...b.footprintENU.map((p) => p[0])), Math.max(...b.footprintENU.map((p) => p[1]))]
- function keys(box) {
- const result = []
- for (let x = Math.floor(box[0] / gridSize); x <= Math.floor(box[2] / gridSize); x++)
- for (let y = Math.floor(box[1] / gridSize); y <= Math.floor(box[3] / gridSize); y++) result.push(`${x},${y}`)
- return result
- }
- for (const id of active) for (const key of keys(bboxENU(planned.get(id)))) {
- if (!grid.has(key)) grid.set(key, [])
- grid.get(key).push(id)
- }
- const candidates = []
- for (const b of plan.buildings.filter((b) => b.areaSqm >= 250 && !active.has(b.id)).sort((a, b) => b.areaSqm - a.areaSqm)) {
- const feature = featureFor(b.id)
- const nearby = new Set(keys(bboxENU(b)).flatMap((key) => grid.get(key) || []))
- let bestOverlap = 0, overlapId = null
- for (const id of nearby) {
- const intersection = turf.intersect(turf.featureCollection([feature, featureFor(id)]))
- const fraction = intersection ? turf.area(intersection) / turf.area(feature) : 0
- if (fraction > bestOverlap) { bestOverlap = fraction; overlapId = id }
- }
- const anchor = plan.samples[b.sampleIndices.at(-1)]
- const status = bestOverlap >= 0.8 ? 'covered-by-overlapping-survey-outline'
- : seatedById.has(b.id) ? 'seated-outside-active-selection' : excluded.get(b.id)?.reason || 'not-exported'
- candidates.push({ id: b.id, tier: b.tier, maskId: b.maskId, areaSqm: b.areaSqm, width: b.width, depth: b.depth,
- center: [anchor.lon, anchor.lat], status, overlapFraction: +bestOverlap.toFixed(4), overlapId,
- terrainRelief: excluded.get(b.id)?.relief ?? seatedById.get(b.id)?.terrainRelief,
- source: '1:500 surveyed JMD polygon; height/facade estimated', imageryReviewed: false,
- geometry: feature.geometry })
- }
- const counts = {}
- for (const b of candidates) counts[b.status] = (counts[b.status] || 0) + 1
- const report = { activeBuildings: active.size, thresholdSqm: 250, sourcePlanSha256: createHash('sha256').update(readFileSync(join(root, 'plan.json'))).digest('hex'),
- candidateCount: candidates.length, urbanCandidates: candidates.filter((b) => b.tier === 'urban').length,
- counts, candidates, caveat: 'Candidate IDs are not confirmed missing buildings. Review imagery, partial overlaps, roads and DEM before adding. Do not restore scattered rural houses wholesale.' }
- writeFileSync(join(out, 'audit.json'), JSON.stringify(report, null, 2))
- writeFileSync(join(out, 'candidates.geojson'), JSON.stringify(turf.featureCollection(candidates.map(({ geometry, ...properties }) => turf.feature(geometry, properties)))))
- console.log(JSON.stringify({ out, activeBuildings: active.size, candidateCount: candidates.length, counts }))
|