probe-gis-sources.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. // Identify the untouched local sources with bounded memory:
  2. // 1) raster header (resolution / extent) without loading the file,
  3. // 2) attribute text inside the large geodatabase tables.
  4. import { openSync, readSync, closeSync, statSync } from 'node:fs'
  5. function tiffSummary(path) {
  6. const size = statSync(path).size
  7. const fd = openSync(path, 'r')
  8. const head = Buffer.alloc(8)
  9. readSync(fd, head, 0, 8, 0)
  10. const little = head.toString('latin1', 0, 2) === 'II'
  11. const ifdOffset = little ? head.readUInt32LE(4) : head.readUInt32BE(4)
  12. const countBuf = Buffer.alloc(2)
  13. readSync(fd, countBuf, 0, 2, ifdOffset)
  14. const count = little ? countBuf.readUInt16LE(0) : countBuf.readUInt16BE(0)
  15. const entries = Buffer.alloc(count * 12)
  16. readSync(fd, entries, 0, entries.length, ifdOffset + 2)
  17. const NAMES = { 256: 'width', 257: 'height', 258: 'bits', 259: 'compression', 33550: 'pixelScale', 33922: 'tiepoint', 34735: 'geoKeys', 34737: 'geoAscii', 42112: 'gdaldoc', 42113: 'nodata' }
  18. const tags = {}
  19. const data = []
  20. for (let i = 0; i < count; i++) {
  21. const base = i * 12
  22. const tag = little ? entries.readUInt16LE(base) : entries.readUInt16BE(base)
  23. const type = little ? entries.readUInt16LE(base + 2) : entries.readUInt16BE(base + 2)
  24. const num = little ? entries.readUInt32LE(base + 4) : entries.readUInt32BE(base + 4)
  25. const sizes = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8 }
  26. const total = (sizes[type] || 1) * num
  27. if (!NAMES[tag]) continue
  28. if (total <= 4) {
  29. const raw = entries.subarray(base + 8, base + 12)
  30. tags[NAMES[tag]] = type === 3 ? (little ? raw.readUInt16LE(0) : raw.readUInt16BE(0)) : (little ? raw.readUInt32LE(0) : raw.readUInt32BE(0))
  31. } else {
  32. const pointer = little ? entries.readUInt32LE(base + 8) : entries.readUInt32BE(base + 8)
  33. const payload = Buffer.alloc(Math.min(total, 4096))
  34. readSync(fd, payload, 0, payload.length, pointer)
  35. if (type === 12) {
  36. const values = []
  37. for (let v = 0; v + 8 <= payload.length && values.length < 16; v += 8)
  38. values.push(little ? payload.readDoubleLE(v) : payload.readDoubleBE(v))
  39. tags[NAMES[tag]] = values
  40. } else if (type === 2) {
  41. tags[NAMES[tag]] = payload.toString('latin1').replace(/\0+$/, '').slice(0, 300)
  42. } else if (type === 3) {
  43. const values = []
  44. for (let v = 0; v + 2 <= payload.length && values.length < 64; v += 2)
  45. values.push(little ? payload.readUInt16LE(v) : payload.readUInt16BE(v))
  46. tags[NAMES[tag]] = values
  47. }
  48. }
  49. }
  50. closeSync(fd)
  51. const scale = tags.pixelScale
  52. const tie = tags.tiepoint
  53. const resolution = scale ? Math.round(scale[0] * 1e6) / 1e6 : null
  54. return {
  55. sizeGB: Math.round((size / 1024 ** 3) * 100) / 100,
  56. littleEndian: little,
  57. width: tags.width,
  58. height: tags.height,
  59. bitsPerSample: tags.bits,
  60. compression: tags.compression,
  61. metresPerPixel: resolution,
  62. upperLeftLonLat: tie && scale ? [tie[3], tie[4]] : null,
  63. extentKm: width2(tags.width, resolution),
  64. }
  65. function width2(w, r) { return w && r ? [Math.round((w * r) / 100) / 10, Math.round((tags.height * r) / 100) / 10] : null }
  66. }
  67. const HINTS = /(建筑|房屋|砖混|砖木|框架|结构|层数|楼层|居民|住宅|楼房|厂房|building|floor|storey|structure)/
  68. /** Scan a big table in chunks, keeping only strings that look like real attribute values. */
  69. function scanTable(path, chunkSize = 4 * 1024 * 1024) {
  70. const size = statSync(path).size
  71. const fd = openSync(path, 'r')
  72. const buffer = Buffer.alloc(chunkSize)
  73. const hints = new Map()
  74. const chinese = new Map()
  75. let offset = 0
  76. let carry = Buffer.alloc(0)
  77. while (offset < size) {
  78. const read = readSync(fd, buffer, 0, chunkSize, offset)
  79. if (read <= 0) break
  80. const data = carry.length ? Buffer.concat([carry, buffer.subarray(0, read)]) : buffer.subarray(0, read)
  81. for (let i = 0; i + 2 <= data.length; i += 2) {
  82. for (const [enc, readCode] of [['be', (k) => data.readUInt16BE(k)], ['le', (k) => data.readUInt16LE(k)]]) {
  83. let s = '', j = i
  84. while (j + 1 < data.length) {
  85. const code = readCode(j)
  86. if (code === 0 || code < 0x20 || code > 0xffe0) break
  87. s += String.fromCharCode(code); j += 2
  88. if (s.length > 24) { s = ''; break }
  89. }
  90. if (s.length < 2 || !/[\u4e00-\u9fff]/.test(s)) continue
  91. if (HINTS.test(s)) hints.set(s, (hints.get(s) || 0) + 1)
  92. else if (chinese.size < 400) chinese.set(s, (chinese.get(s) || 0) + 1)
  93. }
  94. }
  95. carry = data.subarray(Math.max(0, data.length - 4))
  96. offset += read
  97. }
  98. closeSync(fd)
  99. return {
  100. sizeMB: Math.round(size / 1024 / 1024),
  101. buildingHints: [...hints.entries()].sort((a, b) => b[1] - a[1]).slice(0, 30),
  102. sampleChinese: [...chinese.keys()].slice(0, 40),
  103. }
  104. }
  105. console.log('===== RASTER D:/GIS/ArcGIS/yuanling/yuanling20.tif')
  106. console.log(JSON.stringify(tiffSummary('D:/GIS/ArcGIS/yuanling/yuanling20.tif'), null, 1))
  107. for (const name of ['a00000009', 'a0000000a', 'a0000000b', 'a0000000c', 'a0000000d']) {
  108. const path = `D:/GIS/ArcGIS/yuanling/DB/YUANLING.gdb/${name}.gdbtable`
  109. console.log(`\n===== GDB TABLE ${name}`)
  110. console.log(JSON.stringify(scanTable(path), null, 1))
  111. }