// Evidence-reviewed additions only; preserves the original seated GIS/DEM contract. import assert from 'node:assert/strict' import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { resolve, join } from 'node:path' import { createHash } from 'node:crypto' import { PNG } from 'pngjs' import jpeg from 'jpeg-js' import { Matrix4, Cartesian3, Cartographic } from 'cesium' const root = resolve('tools/city-massing/city/cad-bim') const ids = ['cad-20562', 'cad-20556', 'cad-20308', 'cad-20534', 'cad-20301', 'cad-20293', 'cad-11179', 'cad-11204', 'cad-11154', 'cad-11167', 'cad-11192', 'cad-11395', 'cad-11386'] const seated = JSON.parse(readFileSync(join(root, 'seated-plan.json'), 'utf8')) const selected = ids.map((id) => { const b = seated.buildings.find((b) => b.id === id); assert.ok(b, id); return { ...b, kind: 'massing' } }) const out = join(root, 'near-estate-pilot-input') mkdirSync(out, { recursive: true }) const textures = {}, sourceTiles = new Map(), zoom = 19, tileCount = 2 ** zoom const transform = Matrix4.fromArray(seated.transform) const slippy = (lon, lat) => [(lon + 180) / 360 * tileCount, (1 - Math.asinh(Math.tan(lat * Math.PI / 180)) / Math.PI) / 2 * tileCount] function tile(x, y) { const key = `${x}/${y}` if (!sourceTiles.has(key)) { const path = `D:/GIS/GIS/yuanlingPic3/${zoom}/${x}/${tileCount - 1 - y}.png` const bytes = readFileSync(path) sourceTiles.set(key, { image: bytes[0] === 0xff ? jpeg.decode(bytes) : PNG.sync.read(bytes), path, sha256: createHash('sha256').update(bytes).digest('hex') }) } return sourceTiles.get(key).image } for (const b of selected) { const low = [0, 1].map((axis) => Math.min(...b.footprintENU.map((p) => p[axis]))) const high = [0, 1].map((axis) => Math.max(...b.footprintENU.map((p) => p[axis]))) // ENU-aligned imagery reprojection, no rotated rectangle or shifted survey outline. const longest = Math.max(high[0] - low[0], high[1] - low[1]) const width = Math.max(32, Math.ceil((high[0] - low[0]) / longest * 256)) const height = Math.max(32, Math.ceil((high[1] - low[1]) / longest * 256)) const image = new PNG({ width, height }) for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) { const world = Matrix4.multiplyByPoint(transform, new Cartesian3(low[0] + (x + 0.5) / width * (high[0] - low[0]), high[1] - (y + 0.5) / height * (high[1] - low[1]), b.positionENU[2]), new Cartesian3()) const point = Cartographic.fromCartesian(world) const pixel = slippy(point.longitude * 180 / Math.PI, point.latitude * 180 / Math.PI).map((v) => v * 256) const tx = Math.floor(pixel[0] / 256), ty = Math.floor(pixel[1] / 256) const source = tile(tx, ty) const sourceIndex = (Math.floor(pixel[1] - ty * 256) * 256 + Math.floor(pixel[0] - tx * 256)) * 4 source.data.copy(image.data, (y * width + x) * 4, sourceIndex, sourceIndex + 4) } const path = join(out, `${b.id}-roof.png`) writeFileSync(path, PNG.sync.write(image)) textures[b.id] = { path, boundsENU: [low, high], source: 'local yuanlingPic3 EPSG3857 TMS z19 reprojected to seated ENU', size: [width, height], caveat: 'Orthophoto roof appearance only; capture date and redistribution rights unknown. Shadows/perspective may be baked in. No facade-photo claim.' } } writeFileSync(join(out, 'roof-textures.json'), JSON.stringify(textures, null, 2)) writeFileSync(join(out, 'seated-plan.json'), JSON.stringify({ ...seated, buildings: selected, excluded: [], batch: { selected: selected.length, source: 'large-building-audit satellite review: central-estate and western-estate', heightsAndFacades: 'estimated from original CAD preparation, not surveyed BIM' } }, null, 2)) writeFileSync(join(out, 'imagery-provenance.json'), JSON.stringify({ zoom, sourceTiles: [...sourceTiles.values()].map(({ image, ...record }) => record), buildings: ids, deploymentPolicy: 'local-only existing GIS mount; no inferred permission for public redistribution' }, null, 2)) console.log(JSON.stringify({ out, buildings: ids.length, textures: Object.keys(textures).length, sourceTiles: sourceTiles.size }))