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,379 @@
1
+ """Colour ramps and classification for analysis results.
2
+
3
+ Equal-interval, quantile and Fisher-Jenks natural breaks, categories by
4
+ frequency, and the colour ramps used by the plugin's viewer.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from typing import Any, Iterable, Optional, Sequence
11
+
12
+ __all__ = [
13
+ "QUALITATIVE",
14
+ "RAMPS",
15
+ "RAMP_LABELS",
16
+ "breaks_for",
17
+ "categories",
18
+ "class_of",
19
+ "equal_breaks",
20
+ "fmt",
21
+ "hex_to_rgb",
22
+ "jenks_breaks",
23
+ "quantile_breaks",
24
+ "ramp_color",
25
+ "ramp_stops",
26
+ "rgb_to_hex",
27
+ ]
28
+
29
+ RAMPS: dict[str, list[str]] = {
30
+ "viridis": [
31
+ "#440154",
32
+ "#482878",
33
+ "#3e4989",
34
+ "#31688e",
35
+ "#26828e",
36
+ "#1f9e89",
37
+ "#35b779",
38
+ "#6ece58",
39
+ "#b5de2b",
40
+ "#fde725",
41
+ ],
42
+ "magma": [
43
+ "#000004",
44
+ "#1c1044",
45
+ "#4f127b",
46
+ "#812581",
47
+ "#b5367a",
48
+ "#e55964",
49
+ "#fb8761",
50
+ "#fec287",
51
+ "#fcfdbf",
52
+ ],
53
+ "inferno": [
54
+ "#000004",
55
+ "#1f0c48",
56
+ "#550f6d",
57
+ "#88226a",
58
+ "#ba3655",
59
+ "#e35933",
60
+ "#f98e09",
61
+ "#f9cb35",
62
+ "#fcffa4",
63
+ ],
64
+ "rdylbu": [
65
+ "#a50026",
66
+ "#d73027",
67
+ "#f46d43",
68
+ "#fdae61",
69
+ "#fee090",
70
+ "#ffffbf",
71
+ "#e0f3f8",
72
+ "#abd9e9",
73
+ "#74add1",
74
+ "#4575b4",
75
+ "#313695",
76
+ ],
77
+ "spectral": [
78
+ "#9e0142",
79
+ "#d53e4f",
80
+ "#f46d43",
81
+ "#fdae61",
82
+ "#fee08b",
83
+ "#ffffbf",
84
+ "#e6f598",
85
+ "#abdda4",
86
+ "#66c2a5",
87
+ "#3288bd",
88
+ "#5e4fa2",
89
+ ],
90
+ "rdbu": [
91
+ "#67001f",
92
+ "#b2182b",
93
+ "#d6604d",
94
+ "#f4a582",
95
+ "#fddbc7",
96
+ "#f7f7f7",
97
+ "#d1e5f0",
98
+ "#92c5de",
99
+ "#4393c3",
100
+ "#2166ac",
101
+ "#053061",
102
+ ],
103
+ "blues": [
104
+ "#f7fbff",
105
+ "#deebf7",
106
+ "#c6dbef",
107
+ "#9ecae1",
108
+ "#6baed6",
109
+ "#4292c6",
110
+ "#2171b5",
111
+ "#08519c",
112
+ "#08306b",
113
+ ],
114
+ "reds": [
115
+ "#fff5f0",
116
+ "#fee0d2",
117
+ "#fcbba1",
118
+ "#fc9272",
119
+ "#fb6a4a",
120
+ "#ef3b2c",
121
+ "#cb181d",
122
+ "#a50f15",
123
+ "#67000d",
124
+ ],
125
+ "greens": [
126
+ "#f7fcf5",
127
+ "#e5f5e0",
128
+ "#c7e9c0",
129
+ "#a1d99b",
130
+ "#74c476",
131
+ "#41ab5d",
132
+ "#238b45",
133
+ "#006d2c",
134
+ "#00441b",
135
+ ],
136
+ "terrain": [
137
+ "#2b6b3f",
138
+ "#5f9a4f",
139
+ "#a8c47a",
140
+ "#e9dfa3",
141
+ "#d2b47c",
142
+ "#a47b55",
143
+ "#8a7a72",
144
+ "#f2f2f2",
145
+ ],
146
+ "greys": [
147
+ "#000000",
148
+ "#262626",
149
+ "#4d4d4d",
150
+ "#737373",
151
+ "#969696",
152
+ "#bdbdbd",
153
+ "#d9d9d9",
154
+ "#f0f0f0",
155
+ "#ffffff",
156
+ ],
157
+ }
158
+ QUALITATIVE: list[str] = [
159
+ "#4e79a7",
160
+ "#f28e2b",
161
+ "#e15759",
162
+ "#76b7b2",
163
+ "#59a14f",
164
+ "#edc948",
165
+ "#b07aa1",
166
+ "#ff9da7",
167
+ "#9c755f",
168
+ "#bab0ac",
169
+ "#1f77b4",
170
+ "#ff7f0e",
171
+ "#2ca02c",
172
+ "#d62728",
173
+ "#9467bd",
174
+ "#8c564b",
175
+ "#e377c2",
176
+ "#7f7f7f",
177
+ "#bcbd22",
178
+ "#17becf",
179
+ ]
180
+ RAMP_LABELS: dict[str, str] = {
181
+ "viridis": "Viridis",
182
+ "magma": "Magma",
183
+ "inferno": "Inferno",
184
+ "rdylbu": "Red-Yellow-Blue",
185
+ "spectral": "Spectral",
186
+ "rdbu": "Red-Blue",
187
+ "blues": "Blues",
188
+ "reds": "Reds",
189
+ "greens": "Greens",
190
+ "terrain": "Terrain",
191
+ "greys": "Greys",
192
+ }
193
+
194
+
195
+ def hex_to_rgb(hex_color: Optional[str]) -> list[float]:
196
+ """``'#rrggbb'`` (or ``'#rgb'``) to ``[r, g, b]`` in ``0..1``."""
197
+ h = str(hex_color or "#cccccc").replace("#", "")
198
+ if len(h) == 3:
199
+ h = "".join(ch + ch for ch in h)
200
+ try:
201
+ v = int(h[:6], 16)
202
+ except ValueError:
203
+ v = 0
204
+ return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255]
205
+
206
+
207
+ def _round_half_up(x: float) -> int:
208
+ return int(math.floor(x + 0.5))
209
+
210
+
211
+ def rgb_to_hex(rgb: Sequence[float]) -> str:
212
+ """``[r, g, b]`` in ``0..1`` to ``'#rrggbb'``."""
213
+ return "#" + "".join(f"{_round_half_up(max(0.0, min(1.0, x)) * 255):02x}" for x in rgb[:3])
214
+
215
+
216
+ def ramp_stops(name: str) -> list[list[float]]:
217
+ """Ramp stops as RGB triples; a name ending in ``-r`` is reversed."""
218
+ name = str(name)
219
+ reversed_ = name.endswith("-r")
220
+ key = name[:-2] if reversed_ else name
221
+ stops = [hex_to_rgb(h) for h in RAMPS.get(key, RAMPS["viridis"])]
222
+ return stops[::-1] if reversed_ else stops
223
+
224
+
225
+ def ramp_color(stops: Sequence[Sequence[float]], t: float) -> list[float]:
226
+ """Colour at ``t`` in ``[0, 1]`` along a ramp (stops from :func:`ramp_stops`)."""
227
+ tt = t if isinstance(t, (int, float)) and math.isfinite(t) else 0.0
228
+ v = max(0.0, min(1.0, tt)) * (len(stops) - 1)
229
+ i = min(len(stops) - 2, int(math.floor(v)))
230
+ f = v - i
231
+ return [stops[i][k] + (stops[i + 1][k] - stops[i][k]) * f for k in range(3)]
232
+
233
+
234
+ def _to_float(v: Any) -> Optional[float]:
235
+ if v is None or v == "":
236
+ return None
237
+ try:
238
+ x = float(v)
239
+ except (TypeError, ValueError):
240
+ return None
241
+ return x if math.isfinite(x) else None
242
+
243
+
244
+ def _finite_sorted(values: Iterable[Any]) -> list[float]:
245
+ out = [x for x in (_to_float(v) for v in values) if x is not None]
246
+ return sorted(out)
247
+
248
+
249
+ def equal_breaks(values: Iterable[Any], k: int) -> list[float]:
250
+ """``k`` equal-interval classes: ``k + 1`` break values."""
251
+ s = _finite_sorted(values)
252
+ if not s:
253
+ return []
254
+ lo, hi = s[0], s[-1]
255
+ return [lo + ((hi - lo) * i) / k for i in range(k + 1)]
256
+
257
+
258
+ def quantile_breaks(values: Iterable[Any], k: int) -> list[float]:
259
+ """``k`` quantile classes: ``k + 1`` break values."""
260
+ s = _finite_sorted(values)
261
+ if not s:
262
+ return []
263
+ out = [s[0]]
264
+ for i in range(1, k):
265
+ out.append(s[min(len(s) - 1, (i * len(s)) // k)])
266
+ out.append(s[-1])
267
+ return out
268
+
269
+
270
+ def jenks_breaks(values: Iterable[Any], k: int) -> list[float]:
271
+ """Fisher-Jenks natural breaks on at most 3000 sampled values."""
272
+ full = _finite_sorted(values)
273
+ s = full
274
+ if not s:
275
+ return []
276
+ if len(s) > 3000:
277
+ step = len(s) / 3000
278
+ s = [full[int(math.floor(i * step))] for i in range(3000)]
279
+ s.append(full[-1])
280
+ n = len(s)
281
+ k = max(1, min(k, len(set(s))))
282
+ inf = math.inf
283
+ lower = [[0] * (k + 1) for _ in range(n + 1)]
284
+ variance = [[inf] * (k + 1) for _ in range(n + 1)]
285
+ for j in range(1, k + 1):
286
+ lower[1][j] = 1
287
+ variance[1][j] = 0.0
288
+ for l_ in range(2, n + 1):
289
+ total = 0.0
290
+ total_sq = 0.0
291
+ w = 0
292
+ v = 0.0
293
+ var_l = variance[l_]
294
+ low_l = lower[l_]
295
+ for m in range(1, l_ + 1):
296
+ i3 = l_ - m + 1
297
+ val = s[i3 - 1]
298
+ w += 1
299
+ total += val
300
+ total_sq += val * val
301
+ v = total_sq - (total * total) / w
302
+ i4 = i3 - 1
303
+ if i4 != 0:
304
+ var_i4 = variance[i4]
305
+ for j in range(2, k + 1):
306
+ cand = v + var_i4[j - 1]
307
+ if var_l[j] >= cand:
308
+ low_l[j] = i3
309
+ var_l[j] = cand
310
+ low_l[1] = 1
311
+ var_l[1] = v
312
+ breaks = [0.0] * (k + 1)
313
+ breaks[k] = s[n - 1]
314
+ breaks[0] = s[0]
315
+ idx = n
316
+ for j in range(k, 1, -1):
317
+ ident = lower[idx][j] - 2
318
+ breaks[j - 1] = s[max(0, ident)]
319
+ idx = lower[idx][j] - 1
320
+ return breaks
321
+
322
+
323
+ def breaks_for(method: str, values: Iterable[Any], k: int) -> list[float]:
324
+ """Breaks by ``method``: ``'quantile'``, ``'jenks'`` or equal interval (default)."""
325
+ if method == "quantile":
326
+ return quantile_breaks(values, k)
327
+ if method == "jenks":
328
+ return jenks_breaks(values, k)
329
+ return equal_breaks(values, k)
330
+
331
+
332
+ def class_of(value: Any, breaks: Sequence[float]) -> int:
333
+ """Class index ``0..k-1`` of a value for breaks ``[b0..bk]``, or ``-1``."""
334
+ v = _to_float(value)
335
+ if v is None or len(breaks) < 2:
336
+ return -1
337
+ for i in range(1, len(breaks) - 1):
338
+ if v < breaks[i]:
339
+ return i - 1
340
+ return len(breaks) - 2
341
+
342
+
343
+ def _key(v: Any) -> str:
344
+ if v is None or v == "":
345
+ return "(empty)"
346
+ if isinstance(v, bool):
347
+ return "true" if v else "false"
348
+ if isinstance(v, float) and v.is_integer():
349
+ return str(int(v))
350
+ return str(v)
351
+
352
+
353
+ def categories(values: Iterable[Any], limit: int = 20) -> list[dict[str, Any]]:
354
+ """Distinct values by frequency, most common first (at most ``limit``)."""
355
+ counts: dict[str, int] = {}
356
+ for v in values:
357
+ key = _key(v)
358
+ counts[key] = counts.get(key, 0) + 1
359
+ ordered = sorted(counts.items(), key=lambda kv: -kv[1])[:limit]
360
+ return [{"value": value, "count": count} for value, count in ordered]
361
+
362
+
363
+ def fmt(v: Any, unit: str = "") -> str:
364
+ """Short number formatting for legends (``'–'`` for non-finite values)."""
365
+ if not isinstance(v, (int, float)) or isinstance(v, bool) or not math.isfinite(v):
366
+ return "–"
367
+ if float(v).is_integer():
368
+ s = f"{int(v):,}"
369
+ else:
370
+ a = abs(v)
371
+ if a >= 1000:
372
+ s = f"{_round_half_up(v):,}"
373
+ elif a >= 100:
374
+ s = f"{v:.0f}"
375
+ elif a >= 10:
376
+ s = f"{v:.1f}"
377
+ else:
378
+ s = f"{v:.2f}"
379
+ return f"{s} {unit}" if unit else s