// 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 }