draw-city-masks.mjs 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. // Draw source residential masks, waterways and main roads over the georeferenced city atlas.
  2. import { readFileSync, writeFileSync } from 'node:fs'
  3. import { resolve } from 'node:path'
  4. import { PNG } from 'pngjs'
  5. const dir = resolve('tools/city-massing/city')
  6. const atlas = JSON.parse(readFileSync(resolve(dir, 'city-inventory.json')))
  7. const image = PNG.sync.read(readFileSync(resolve(dir, 'city-satellite.png')))
  8. const b = atlas.bounds,
  9. z = atlas.imagery.zoom,
  10. n = 2 ** z,
  11. tile = 256
  12. function project([lon, lat]) {
  13. const x = ((lon + 180) / 360) * n * tile,
  14. y = ((1 - Math.asinh(Math.tan((lat * Math.PI) / 180)) / Math.PI) / 2) * n * tile
  15. return [x - atlas.imagery.westTile * tile, y - atlas.imagery.googleY0 * tile]
  16. }
  17. function line(a, b, color, width = 2) {
  18. const len = Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1]))
  19. for (let i = 0; i <= len; i++) {
  20. const x = Math.round(a[0] + ((b[0] - a[0]) * i) / len),
  21. y = Math.round(a[1] + ((b[1] - a[1]) * i) / len)
  22. for (let dy = -width; dy <= width; dy++)
  23. for (let dx = -width; dx <= width; dx++) {
  24. const px = x + dx,
  25. py = y + dy
  26. if (px < 0 || py < 0 || px >= image.width || py >= image.height) continue
  27. const k = (py * image.width + px) * 4
  28. image.data[k] = color[0]
  29. image.data[k + 1] = color[1]
  30. image.data[k + 2] = color[2]
  31. image.data[k + 3] = 255
  32. }
  33. }
  34. }
  35. const layers = [
  36. ['residential-clipped.geojson', [255, 220, 0], 3],
  37. ['water-clipped.geojson', [0, 210, 255], 3],
  38. ['osm-buildings-clipped.geojson', [255, 30, 30], 2],
  39. ]
  40. for (const [file, color, width] of layers) {
  41. const j = JSON.parse(readFileSync(resolve(dir, file)))
  42. for (const f of j.features) {
  43. if (f.geometry.type === 'Polygon')
  44. for (const ring of f.geometry.coordinates)
  45. for (let i = 0; i < ring.length - 1; i++) line(project(ring[i]), project(ring[i + 1]), color, width)
  46. if (f.geometry.type === 'MultiPolygon')
  47. for (const poly of f.geometry.coordinates)
  48. for (const ring of poly)
  49. for (let i = 0; i < ring.length - 1; i++) line(project(ring[i]), project(ring[i + 1]), color, width)
  50. }
  51. }
  52. const roads = JSON.parse(readFileSync(resolve(dir, 'roads-clipped.geojson')))
  53. for (const f of roads.features) {
  54. const c = f.geometry.coordinates
  55. if (f.geometry.type === 'LineString')
  56. for (let i = 1; i < c.length; i++) line(project(c[i - 1]), project(c[i]), [255, 0, 255], 1)
  57. else if (f.geometry.type === 'MultiLineString')
  58. for (const l of c) for (let i = 1; i < l.length; i++) line(project(l[i - 1]), project(l[i]), [255, 0, 255], 1)
  59. }
  60. writeFileSync(resolve(dir, 'city-inventory-overlay.png'), PNG.sync.write(image))
  61. console.log(
  62. JSON.stringify({
  63. size: [image.width, image.height],
  64. bbox: b,
  65. imagery: atlas.imagery,
  66. layerCounts: {
  67. residential: atlas.originalSources.residential,
  68. water: atlas.originalSources.water,
  69. roads: atlas.originalSources.osmRoads,
  70. osmBuildings: atlas.originalSources.osmBuildings,
  71. },
  72. })
  73. )