zero2geospatial3d-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. zero2geospatial3d/__init__.py +105 -0
  2. zero2geospatial3d/acoustics.py +284 -0
  3. zero2geospatial3d/classify.py +379 -0
  4. zero2geospatial3d/cli.py +367 -0
  5. zero2geospatial3d/comfort.py +687 -0
  6. zero2geospatial3d/data/epw_stations.json.gz +0 -0
  7. zero2geospatial3d/daylight.py +337 -0
  8. zero2geospatial3d/dispersion.py +249 -0
  9. zero2geospatial3d/epw.py +162 -0
  10. zero2geospatial3d/geo.py +138 -0
  11. zero2geospatial3d/grid.py +119 -0
  12. zero2geospatial3d/microclimate.py +388 -0
  13. zero2geospatial3d/morphology.py +283 -0
  14. zero2geospatial3d/nsga2.py +315 -0
  15. zero2geospatial3d/osm.py +450 -0
  16. zero2geospatial3d/osm_tags.py +180 -0
  17. zero2geospatial3d/py.typed +0 -0
  18. zero2geospatial3d/results.py +231 -0
  19. zero2geospatial3d/road_sources.py +141 -0
  20. zero2geospatial3d/scene.py +555 -0
  21. zero2geospatial3d/server.py +332 -0
  22. zero2geospatial3d/shading.py +443 -0
  23. zero2geospatial3d/solar.py +207 -0
  24. zero2geospatial3d/terrain.py +610 -0
  25. zero2geospatial3d/urban_form.py +326 -0
  26. zero2geospatial3d/walkability.py +380 -0
  27. zero2geospatial3d/weather.py +373 -0
  28. zero2geospatial3d/web/index.html +50 -0
  29. zero2geospatial3d/web/src/analysis/climate.js +278 -0
  30. zero2geospatial3d/web/src/analysis/compliance.js +345 -0
  31. zero2geospatial3d/web/src/analysis/gpu.js +431 -0
  32. zero2geospatial3d/web/src/analysis/optimize.js +208 -0
  33. zero2geospatial3d/web/src/analysis/runners.js +1644 -0
  34. zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
  35. zero2geospatial3d/web/src/app.js +630 -0
  36. zero2geospatial3d/web/src/core/acoustics.js +341 -0
  37. zero2geospatial3d/web/src/core/classify.js +158 -0
  38. zero2geospatial3d/web/src/core/comfort.js +686 -0
  39. zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
  40. zero2geospatial3d/web/src/core/dispersion.js +264 -0
  41. zero2geospatial3d/web/src/core/microclimate.js +372 -0
  42. zero2geospatial3d/web/src/core/morphology.js +153 -0
  43. zero2geospatial3d/web/src/core/nsga2.js +181 -0
  44. zero2geospatial3d/web/src/core/party_walls.js +60 -0
  45. zero2geospatial3d/web/src/core/road_sources.js +127 -0
  46. zero2geospatial3d/web/src/core/solar.js +112 -0
  47. zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
  48. zero2geospatial3d/web/src/core/terrain_math.js +279 -0
  49. zero2geospatial3d/web/src/core/urban_form.js +405 -0
  50. zero2geospatial3d/web/src/core/walkability.js +316 -0
  51. zero2geospatial3d/web/src/core/weather.js +225 -0
  52. zero2geospatial3d/web/src/core/wind.js +131 -0
  53. zero2geospatial3d/web/src/export/check_pdf.js +74 -0
  54. zero2geospatial3d/web/src/export/exporters.js +556 -0
  55. zero2geospatial3d/web/src/export/pdf.js +149 -0
  56. zero2geospatial3d/web/src/export/snapshot.js +234 -0
  57. zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
  58. zero2geospatial3d/web/src/main.js +75 -0
  59. zero2geospatial3d/web/src/scene/objects.js +645 -0
  60. zero2geospatial3d/web/src/scene/overlay.js +145 -0
  61. zero2geospatial3d/web/src/scene/terrain.js +546 -0
  62. zero2geospatial3d/web/src/scene/viewer.js +275 -0
  63. zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
  64. zero2geospatial3d/web/src/style.css +225 -0
  65. zero2geospatial3d/web/src/ui/building_report.js +404 -0
  66. zero2geospatial3d/web/src/ui/charts.js +441 -0
  67. zero2geospatial3d/web/src/ui/colorize.js +101 -0
  68. zero2geospatial3d/web/src/ui/compare.js +362 -0
  69. zero2geospatial3d/web/src/ui/dom.js +104 -0
  70. zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
  71. zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
  72. zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
  73. zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
  74. zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
  75. zero2geospatial3d/web/src/ui/sunclock.js +45 -0
  76. zero2geospatial3d/web/src/ui/tools.js +197 -0
  77. zero2geospatial3d/web/src/ui/viewtools.js +173 -0
  78. zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
  79. zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
  80. zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
  81. zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
  82. zero2geospatial3d/wind.py +178 -0
  83. zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
  84. zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
  85. zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
  86. zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  87. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  88. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
  89. zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,145 @@
1
+ // @ts-check
2
+ // Terrain analyses as results: elevation, slope, aspect, hillshade,
3
+ // curvature, topographic position, ruggedness and wetness of the DEM. Each is
4
+ // a grid result like sun hours or sky view, so it can be drawn on the ground,
5
+ // put in a compare panel and sent to QGIS as a GeoTIFF.
6
+ import { slopeAspect, hillshade, gridStats } from '../core/terrain_math.js';
7
+ import { cropGrid, curvature, tpi, ruggedness, aspectClasses, flowAccumulation, wetness, percentile } from '../core/terrain_analysis.js';
8
+
9
+ export const TERRAIN_ANALYSES = [
10
+ { id: 'elevation', label: 'Elevation', help: 'Height above sea level of the DEM.' },
11
+ { id: 'slope', label: 'Slope', help: 'Steepness in degrees (Horn 1981), with the area in slope bands.' },
12
+ { id: 'aspect', label: 'Aspect', help: 'The compass direction the slope faces, in eight classes; ground under 1° is flat.' },
13
+ { id: 'hillshade', label: 'Hillshade', help: 'Relief lit from the north-west at 45°.' },
14
+ { id: 'curvature', label: 'Curvature', help: 'Convex ground (crests, ridges) positive, concave ground (hollows, valleys) negative.' },
15
+ { id: 'tpi', label: 'Topographic position', help: 'Height above (+) or below (−) the mean of the surroundings within the radius: ridges and valleys at that scale.' },
16
+ { id: 'tri', label: 'Ruggedness', help: 'Terrain ruggedness index (Riley 1999): how much the height changes between neighbouring cells.' },
17
+ { id: 'twi', label: 'Wetness index', help: 'Topographic wetness index ln(a / tan β): where water collects and the ground stays wet. From the DEM alone, without soil or drains.' }
18
+ ];
19
+
20
+ export const ASPECT_COLORS = { 0: '#b8b8b8', 1: '#e63b2e', 2: '#f39a2b', 3: '#f2df3a', 4: '#78c04a', 5: '#3cb7a4', 6: '#3f8fd2', 7: '#5b55b5', 8: '#c24d9c' };
21
+ export const ASPECT_LABELS = { 0: 'Flat', 1: 'North', 2: 'North-east', 3: 'East', 4: 'South-east', 5: 'South', 6: 'South-west', 7: 'West', 8: 'North-west' };
22
+
23
+ const pct = (n, total) => `${((100 * n) / Math.max(1, total)).toFixed(1)} %`;
24
+ const tick = () => new Promise((r) => setTimeout(r, 0));
25
+
26
+ /** A symmetric range around 0 that holds 98 % of the values. */
27
+ function symmetric(values, floor) {
28
+ const m = Math.max(Math.abs(percentile(values, 0.01)), Math.abs(percentile(values, 0.99)), floor);
29
+ return { min: -m, max: m };
30
+ }
31
+
32
+ /**
33
+ * Run the chosen terrain analyses.
34
+ * @param {any} app
35
+ * @param {{ kinds: string[], extent?: 'scene' | 'area', tpiRadius?: number, maxCells?: number, onProgress?: (f: number) => void }} o
36
+ */
37
+ export async function terrainAnalyses(app, { kinds, extent = 'scene', tpiRadius = 100, maxCells = 4_000_000, onProgress }) {
38
+ if (!kinds.length) throw new Error('Tick at least one terrain analysis.');
39
+ const t = app.terrain;
40
+ const area = extent === 'area' ? app.area() : app.sceneArea();
41
+ const g = cropGrid(t.dataGrid, area, maxCells);
42
+ const where = `${extent === 'area' ? 'Analysis area' : 'Whole DEM'}, ${g.cell.toFixed(g.cell < 10 ? 1 : 0)} m cells${g.factor > 1 ? ` (DEM ×${g.factor} coarser)` : ''}`;
43
+ const want = new Set(kinds);
44
+ const steps = kinds.length + (want.has('twi') ? 1 : 0);
45
+ let done = 0;
46
+ const step = async () => { onProgress?.(++done / steps); await tick(); };
47
+ // groundOnly: a map of the ground itself is not drawn on roofs.
48
+ const base = (title, values, unit, display, extra = {}) => ({ kind: 'terrain', title, subtitle: where, unit, grid: g, values, display, fields: [], groundOnly: true, ...extra });
49
+ const out = [];
50
+ let sa = null;
51
+ const slopeAsp = () => (sa ??= slopeAspect(g));
52
+ const elevStats = gridStats(g.values);
53
+ if (want.has('elevation')) {
54
+ out.push(base('Elevation', g.values, 'm', { min: elevStats.min, max: elevStats.max, ramp: 'terrain' }, {
55
+ stats: [['Lowest', `${elevStats.min.toFixed(1)} m`], ['Highest', `${elevStats.max.toFixed(1)} m`], ['Mean', `${elevStats.mean.toFixed(1)} m`],
56
+ ['Relief', `${(elevStats.max - elevStats.min).toFixed(1)} m`]] }));
57
+ await step();
58
+ }
59
+ if (want.has('slope')) {
60
+ const { slope } = slopeAsp();
61
+ // Bands in percent rise (100 % = 45°), as slopes are usually quoted in planning.
62
+ const bands = [5, 10, 15, 30];
63
+ const counts = new Array(bands.length + 1).fill(0);
64
+ let n = 0;
65
+ for (const s of slope) {
66
+ if (!Number.isFinite(s)) continue;
67
+ n++;
68
+ const rise = 100 * Math.tan((s * Math.PI) / 180);
69
+ let k = 0;
70
+ while (k < bands.length && rise >= bands[k]) k++;
71
+ counts[k]++;
72
+ }
73
+ const st = gridStats(slope);
74
+ const labels = ['under 5 %', '5–10 %', '10–15 %', '15–30 %', '30 % and over'];
75
+ out.push(base('Slope', slope, '°', { min: 0, max: Math.max(5, percentile(slope, 0.99)), ramp: 'spectral-r' }, {
76
+ stats: [['Mean', `${st.mean.toFixed(1)}° (${(100 * Math.tan((st.mean * Math.PI) / 180)).toFixed(0)} %)`], ['Steepest', `${st.max.toFixed(1)}°`],
77
+ ...labels.map((l, k) => [`Area ${l}`, pct(counts[k], n)])],
78
+ notes: 'Percent rise = 100 × tan(slope); 100 % is 45°.' }));
79
+ await step();
80
+ }
81
+ if (want.has('aspect')) {
82
+ const { slope, aspect } = slopeAsp();
83
+ const cls = aspectClasses(slope, aspect, 1);
84
+ const counts = new Map();
85
+ let n = 0;
86
+ for (const v of cls) if (Number.isFinite(v)) { counts.set(v, (counts.get(v) || 0) + 1); n++; }
87
+ const keys = [...counts.keys()].sort((a, b) => a - b);
88
+ out.push(base('Aspect', cls, '', { min: 0, max: 8, categorical: ASPECT_COLORS, labels: ASPECT_LABELS }, {
89
+ legend: keys.map((k) => ({ color: ASPECT_COLORS[k], label: ASPECT_LABELS[k] })),
90
+ stats: keys.map((k) => [ASPECT_LABELS[k], pct(counts.get(k), n)]),
91
+ notes: 'The direction a slope faces (downhill). South-facing slopes get the most sun in the northern hemisphere.' }));
92
+ await step();
93
+ }
94
+ if (want.has('hillshade')) {
95
+ out.push(base('Hillshade', hillshade(g, 315, 45, 1), '', { min: 0, max: 1, ramp: 'greys' }, { stats: [['Light from', 'north-west, 45° high']] }));
96
+ await step();
97
+ }
98
+ if (want.has('curvature')) {
99
+ const cv = curvature(g);
100
+ const st = gridStats(cv);
101
+ out.push(base('Curvature', cv, '1/100 m', { ...symmetric(cv, 0.01), ramp: 'rdbu-r' }, {
102
+ stats: [['Most convex', st.max.toFixed(3)], ['Most concave', st.min.toFixed(3)]],
103
+ notes: 'General curvature (Zevenbergen and Thorne 1987) in 1/100 m. Red: convex (crests); blue: concave (hollows), where water and cold air gather.' }));
104
+ await step();
105
+ }
106
+ if (want.has('tpi')) {
107
+ const R = Math.max(1, Math.round(tpiRadius / g.cell));
108
+ const v = tpi(g, R);
109
+ let ridge = 0, valley = 0, n = 0;
110
+ for (const x of v) { if (!Number.isFinite(x)) continue; n++; if (x > 1) ridge++; else if (x < -1) valley++; }
111
+ out.push(base(`Topographic position (${Math.round(R * g.cell)} m)`, v, 'm', { ...symmetric(v, 0.5), ramp: 'rdbu-r' }, {
112
+ stats: [['Above surroundings by 1 m+', pct(ridge, n)], ['Below surroundings by 1 m+', pct(valley, n)], ['Window', `${2 * R + 1} × ${2 * R + 1} cells`]],
113
+ notes: 'Height minus the mean height within the radius (Weiss 2001). Red: ridges and hilltops at that scale; blue: valleys and hollows.' }));
114
+ await step();
115
+ }
116
+ if (want.has('tri')) {
117
+ const v = ruggedness(g);
118
+ const st = gridStats(v);
119
+ out.push(base('Ruggedness', v, 'm', { min: 0, max: Math.max(0.1, percentile(v, 0.99)), ramp: 'magma-r' }, {
120
+ stats: [['Mean', `${st.mean.toFixed(2)} m`], ['Highest', `${st.max.toFixed(2)} m`]],
121
+ notes: 'Terrain ruggedness index (Riley et al. 1999) at the cell size; it grows with the cell size.' }));
122
+ await step();
123
+ }
124
+ if (want.has('twi')) {
125
+ const { slope } = slopeAsp();
126
+ const { acc } = flowAccumulation(g);
127
+ await step();
128
+ const v = wetness(g, slope, acc);
129
+ const st = gridStats(v);
130
+ const hi = percentile(v, 0.9);
131
+ out.push(base('Wetness index', v, '', { min: percentile(v, 0.02), max: percentile(v, 0.99), ramp: 'blues' }, {
132
+ stats: [['Mean', st.mean.toFixed(2)], ['Wettest 10 % above', hi.toFixed(2)], ['Largest catchment', `${((maxOf(acc) * g.cell * g.cell) / 10000).toFixed(1)} ha`]],
133
+ notes: 'ln(a / tan β) (Beven and Kirkby 1979): D8 flow on the depression-filled DEM. Buildings, drains and soil are not in it; a screening map of where water gathers. Near the DEM edge catchments are cut short, so values there are less reliable.' }));
134
+ await step();
135
+ }
136
+ onProgress?.(1);
137
+ return out;
138
+ }
139
+
140
+ /** The largest finite value, without spreading a huge array into Math.max. */
141
+ function maxOf(values) {
142
+ let m = -Infinity;
143
+ for (const v of values) if (Number.isFinite(v) && v > m) m = v;
144
+ return m;
145
+ }