| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182 |
- // Sample the original local quantized-mesh DEM directly through Cesium; no dev server or interpolated grid.
- import { readFileSync, writeFileSync, existsSync } from 'node:fs'
- import { resolve, relative } from 'node:path'
- import { createHash } from 'node:crypto'
- import { CesiumTerrainProvider, Cartographic, sampleTerrainMostDetailed } from 'cesium'
- const planFile = resolve(process.argv[2] || 'tools/city-massing/city/regular-estate/plan.json')
- const output = resolve(process.argv[3] || 'tools/city-massing/city/regular-estate/terrain-samples.json')
- const dem = resolve(process.env.YUANLING_DEM || 'D:/GIS/GIS/yuanlingDEM')
- const plan = JSON.parse(readFileSync(planFile, 'utf8'))
- if (!Array.isArray(plan.samples) || !plan.samples.length) throw new Error('Plan has no footprint samples')
- const origin = 'https://local-yuanling-dem.invalid/gis/dem/'
- const originalFetch = globalThis.fetch
- globalThis.fetch = (input, init) => {
- const url = String(input?.url || input)
- if (!url.startsWith(origin)) return originalFetch(input, init)
- const path = resolve(dem, decodeURIComponent(new URL(url).pathname.slice('/gis/dem/'.length)))
- if (relative(dem, path).startsWith('..')) throw new Error(`Unsafe local terrain path: ${url}`)
- if (!existsSync(path)) throw new Error(`Missing original terrain tile: ${path}`)
- return Promise.resolve(
- new Response(readFileSync(path), {
- status: 200,
- headers: { 'Content-Type': path.endsWith('.json') ? 'application/json' : 'application/octet-stream' },
- })
- )
- }
- try {
- const provider = await CesiumTerrainProvider.fromUrl(origin)
- const heights = []
- let missing = 0
- for (let i = 0; i < plan.samples.length; i += 128) {
- const points = plan.samples.slice(i, i + 128).map((p) => Cartographic.fromDegrees(p.lon, p.lat))
- const values = await sampleTerrainMostDetailed(provider, points, false)
- for (const value of values) {
- // A sample outside the DEM's coverage is recorded as null and reported, never guessed.
- // The seating stage then excludes the specific module instead of failing the whole city.
- if (!Number.isFinite(value.height) || value.height <= 1) {
- missing++
- heights.push(null)
- } else {
- heights.push(value.height)
- }
- }
- }
- const digest = createHash('sha256').update(JSON.stringify(plan.samples)).digest('hex')
- writeFileSync(
- output,
- JSON.stringify(
- {
- source: dem,
- layerJsonSha256: createHash('sha256')
- .update(readFileSync(resolve(dem, 'layer.json')))
- .digest('hex'),
- samplesSha256: digest,
- points: plan.samples,
- heights,
- missingSamples: missing,
- },
- null,
- 2
- )
- )
- // Extremes by loop: spreading hundreds of thousands of values overflows the call stack.
- let minHeight = null
- let maxHeight = null
- for (const height of heights) {
- if (height === null) continue
- if (minHeight === null || height < minHeight) minHeight = height
- if (maxHeight === null || height > maxHeight) maxHeight = height
- }
- console.log(
- JSON.stringify({
- points: heights.length,
- missingSamples: missing,
- minHeight,
- maxHeight,
- output,
- })
- )
- } finally {
- globalThis.fetch = originalFetch
- }
|