prepare-near-estate-pilot.mjs 4.1 KB

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