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,367 @@
1
+ """Command-line interface: ``zero2geospatial3d <command> ...``.
2
+
3
+ Commands:
4
+ sun sunrise, sunset and noon elevation for a place and date
5
+ stations nearest TMYx weather stations from the bundled index
6
+ epw download a station's EPW file (climate.onebuilding.org)
7
+ comfort UTCI and PET for one set of conditions
8
+ osm download OSM buildings/trees/roads and write a scene package
9
+ demo write a small synthetic scene package
10
+ analyze run a raster analysis on a scene package
11
+ view open a scene package in the bundled browser viewer
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import datetime as _dt
18
+ import json
19
+ import os
20
+ import sys
21
+ from typing import Optional, Sequence
22
+
23
+ import numpy as np
24
+
25
+ from . import __version__
26
+
27
+ ANALYSES = (
28
+ "sun-hours",
29
+ "shadow",
30
+ "svf",
31
+ "viewshed",
32
+ "irradiation",
33
+ "slope",
34
+ "aspect",
35
+ "hillshade",
36
+ "flood",
37
+ )
38
+
39
+
40
+ def _date(text: str) -> _dt.date:
41
+ try:
42
+ return _dt.date.fromisoformat(text)
43
+ except ValueError as exc:
44
+ raise argparse.ArgumentTypeError(f"invalid date {text!r}; use YYYY-MM-DD") from exc
45
+
46
+
47
+ def _fmt_hour(h: Optional[float]) -> str:
48
+ if h is None:
49
+ return "-"
50
+ m = int(round(h * 60))
51
+ return f"{m // 60:02d}:{m % 60:02d}"
52
+
53
+
54
+ def _cmd_sun(args: argparse.Namespace) -> int:
55
+ from . import geo, solar
56
+
57
+ tz = geo.utc_offset_guess(args.lon) if args.tz is None else args.tz
58
+ doy = args.date.timetuple().tm_yday
59
+ rise, sset = solar.sunrise_sunset(doy, args.lat, args.lon, tz)
60
+ best = max(
61
+ (solar.solar_position(doy, h / 60, args.lat, args.lon, tz) for h in range(0, 24 * 60, 2)),
62
+ key=lambda p: p.elevation,
63
+ )
64
+ out = {
65
+ "date": args.date.isoformat(),
66
+ "utc_offset": tz,
67
+ "sunrise": _fmt_hour(rise),
68
+ "sunset": _fmt_hour(sset),
69
+ "day_length_h": round(sset - rise, 2) if rise is not None and sset is not None else None,
70
+ "max_elevation_deg": round(float(np.degrees(best.elevation)), 2),
71
+ }
72
+ print(json.dumps(out, indent=1))
73
+ return 0
74
+
75
+
76
+ def _cmd_stations(args: argparse.Namespace) -> int:
77
+ from . import epw
78
+
79
+ for row in epw.nearest_stations(args.lat, args.lon, args.count):
80
+ print(
81
+ f"{row['distance_km']:8.1f} km UTC{row['utc']:+g} {row['station']}\n {row['url']}"
82
+ )
83
+ return 0
84
+
85
+
86
+ def _cmd_epw(args: argparse.Namespace) -> int:
87
+ from . import epw
88
+
89
+ url = args.url
90
+ if url is None:
91
+ if args.lat is None or args.lon is None:
92
+ print("error: give --url or --lat/--lon", file=sys.stderr)
93
+ return 2
94
+ url = epw.nearest_stations(args.lat, args.lon, 1)[0]["url"]
95
+ path = epw.download_station(url, args.out, timeout=args.timeout)
96
+ print(json.dumps(epw.read_header(path), indent=1))
97
+ return 0
98
+
99
+
100
+ def _cmd_comfort(args: argparse.Namespace) -> int:
101
+ from . import comfort
102
+
103
+ tr = args.tr if args.tr is not None else args.ta
104
+ u = float(comfort.utci(args.ta, tr, args.v, args.rh))
105
+ p = float(comfort.pet(args.ta, tr, args.v, args.rh))
106
+ uc = comfort.utci_category(u)
107
+ pc = comfort.pet_category(p)
108
+ print(
109
+ json.dumps(
110
+ {
111
+ "utci_c": round(u, 2),
112
+ "utci_class": uc["label"] if uc else None,
113
+ "pet_c": round(p, 2),
114
+ "pet_class": pc["label"] if pc else None,
115
+ },
116
+ indent=1,
117
+ )
118
+ )
119
+ return 0
120
+
121
+
122
+ def _cmd_osm(args: argparse.Namespace) -> int:
123
+ from . import geo, osm
124
+ from .scene import Scene
125
+
126
+ bbox = geo.bbox_around(args.lon, args.lat, args.half_size)
127
+ payload = osm.fetch(*bbox, progress=lambda m: print(m, file=sys.stderr), timeout=args.timeout)
128
+ feats = osm.payload_to_features(payload, bbox)
129
+ scene = Scene.from_osm(
130
+ feats,
131
+ lat=args.lat,
132
+ lon=args.lon,
133
+ half_size=args.half_size,
134
+ cell=args.cell,
135
+ utc_offset=args.tz,
136
+ title=args.title,
137
+ )
138
+ scene.export(args.out)
139
+ print(
140
+ f"{len(feats['buildings'])} buildings, {len(feats['trees'])} trees, "
141
+ f"{len(feats['roads'])} roads -> {args.out}"
142
+ )
143
+ return 0
144
+
145
+
146
+ def _demo_scene(size: float = 200.0, cell: float = 2.0):
147
+ from .scene import Building, Scene, Tree
148
+
149
+ scene = Scene.flat(size, size, cell, lat=38.42, lon=27.14, utc_offset=3.0, title="Demo block")
150
+ xx, yy = scene.terrain.coords()
151
+ scene.terrain.values[:] = 10 + 4 * np.exp(-((xx - 40) ** 2 + (yy - 30) ** 2) / (2 * 60.0**2))
152
+ k = 0
153
+ for i, cx in enumerate((-60, -20, 20, 60)):
154
+ for j, cy in enumerate((-60, -20, 20, 60)):
155
+ if (i, j) in ((1, 1), (2, 2)):
156
+ continue
157
+ k += 1
158
+ h = 6.0 + 3.2 * ((i * 3 + j * 5) % 8)
159
+ ring = [
160
+ [cx - 12, cy - 9],
161
+ [cx + 12, cy - 9],
162
+ [cx + 12, cy + 9],
163
+ [cx - 12, cy + 9],
164
+ [cx - 12, cy - 9],
165
+ ]
166
+ scene.buildings.append(Building([[ring]], height=h, id=k, properties={"name": f"B{k}"}))
167
+ for t in range(8):
168
+ scene.trees.append(Tree(-20 + 6 * t, -20 + 3 * (t % 3), 8.0, 3.0))
169
+ return scene
170
+
171
+
172
+ def _cmd_demo(args: argparse.Namespace) -> int:
173
+ scene = _demo_scene(args.size, args.cell)
174
+ scene.export(args.out)
175
+ print(f"demo scene with {len(scene.buildings)} buildings -> {args.out}")
176
+ return 0
177
+
178
+
179
+ def _save(values: np.ndarray, grid, path: str, origin) -> None:
180
+ from .results import write_ascii_grid, write_grid_npy
181
+
182
+ if path.lower().endswith(".asc"):
183
+ write_ascii_grid(path, grid, values, origin=origin)
184
+ elif path.lower().endswith((".tif", ".tiff")):
185
+ from .results import write_geotiff
186
+
187
+ write_geotiff(path, grid, values, origin=origin)
188
+ else:
189
+ write_grid_npy(path, grid, values)
190
+
191
+
192
+ def _cmd_analyze(args: argparse.Namespace) -> int:
193
+ from . import shading, terrain
194
+ from .results import grid_stats
195
+ from .scene import load_scene
196
+
197
+ scene = load_scene(args.scene)
198
+ surface = scene.surface(include_trees=args.trees)
199
+ date = args.date
200
+ kind = args.analysis
201
+ if kind == "sun-hours":
202
+ values = shading.sun_hours(
203
+ surface,
204
+ scene.lat,
205
+ scene.lon,
206
+ scene.utc_offset,
207
+ month=date.month,
208
+ day=date.day,
209
+ start_hour=args.start,
210
+ end_hour=args.end,
211
+ step_minutes=args.step,
212
+ )
213
+ elif kind == "shadow":
214
+ from .solar import solar_position
215
+
216
+ p = solar_position(
217
+ date.timetuple().tm_yday, args.hour, scene.lat, scene.lon, scene.utc_offset
218
+ )
219
+ values = (~shading.shadow_mask(surface, p.elevation, p.azimuth)).astype(float)
220
+ elif kind == "svf":
221
+ values = shading.sky_view_factor(surface, args.azimuths)
222
+ elif kind == "viewshed":
223
+ if args.x is None or args.y is None:
224
+ print("error: viewshed needs --x and --y (local metres)", file=sys.stderr)
225
+ return 2
226
+ values = shading.viewshed(surface, (args.x, args.y), radius=args.radius)
227
+ elif kind == "irradiation":
228
+ from .weather import annual_sun_bins, clear_sky_year, read_epw
229
+
230
+ w = (
231
+ read_epw(args.epw)
232
+ if args.epw
233
+ else clear_sky_year(scene.lat, scene.lon, scene.utc_offset)
234
+ )
235
+ bins = annual_sun_bins(w, scene.lat, scene.lon, scene.utc_offset)
236
+ res = shading.solar_irradiation(
237
+ surface,
238
+ [(d["dir"], d["weight"]) for d in bins["directions"]],
239
+ bins["dhi_sum"],
240
+ bins["ghi_sum"],
241
+ n_azimuths=args.azimuths,
242
+ )
243
+ values = res.total
244
+ elif kind in ("slope", "aspect"):
245
+ slope, aspect = terrain.slope_aspect(scene.terrain)
246
+ values = np.asarray(slope if kind == "slope" else aspect, dtype=float)
247
+ elif kind == "hillshade":
248
+ values = np.asarray(terrain.hillshade(scene.terrain), dtype=float)
249
+ else: # flood
250
+ if args.level is None:
251
+ print("error: flood needs --level (m)", file=sys.stderr)
252
+ return 2
253
+ flooded = terrain.flood(scene.terrain, args.level)
254
+ values = np.asarray(flooded.depth, dtype=float)
255
+ values = np.asarray(values, dtype=float).reshape(scene.terrain.shape)
256
+ stats = grid_stats(values)
257
+ if args.out:
258
+ _save(values, scene.terrain, args.out, scene.origin)
259
+ stats["file"] = args.out
260
+ stats["analysis"] = kind
261
+ print(json.dumps(stats, indent=1))
262
+ return 0
263
+
264
+
265
+ def _cmd_view(args: argparse.Namespace) -> int:
266
+ from .server import serve_scene
267
+
268
+ serve_scene(
269
+ args.scene, open_browser=not args.no_browser, results_dir=args.results or "", port=args.port
270
+ )
271
+ return 0
272
+
273
+
274
+ def build_parser() -> argparse.ArgumentParser:
275
+ parser = argparse.ArgumentParser(
276
+ prog="zero2geospatial3d",
277
+ description="Headless 3D terrain and building analysis (sun, shade, comfort, terrain).",
278
+ )
279
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
280
+ sub = parser.add_subparsers(dest="command", required=True)
281
+
282
+ p = sub.add_parser("sun", help="sunrise, sunset and noon elevation")
283
+ p.add_argument("--lat", type=float, required=True)
284
+ p.add_argument("--lon", type=float, required=True)
285
+ p.add_argument("--date", type=_date, default=_dt.date(2026, 3, 21))
286
+ p.add_argument("--tz", type=float, default=None, help="UTC offset of the clock (h)")
287
+ p.set_defaults(func=_cmd_sun)
288
+
289
+ p = sub.add_parser("stations", help="nearest TMYx weather stations")
290
+ p.add_argument("--lat", type=float, required=True)
291
+ p.add_argument("--lon", type=float, required=True)
292
+ p.add_argument("-n", "--count", type=int, default=5)
293
+ p.set_defaults(func=_cmd_stations)
294
+
295
+ p = sub.add_parser("epw", help="download an EPW weather file")
296
+ p.add_argument("--url", default=None, help="station zip URL (from 'stations')")
297
+ p.add_argument("--lat", type=float, default=None)
298
+ p.add_argument("--lon", type=float, default=None)
299
+ p.add_argument("--out", required=True, help="target .epw path")
300
+ p.add_argument("--timeout", type=float, default=120.0)
301
+ p.set_defaults(func=_cmd_epw)
302
+
303
+ p = sub.add_parser("comfort", help="UTCI and PET for one set of conditions")
304
+ p.add_argument("--ta", type=float, required=True, help="air temperature (C)")
305
+ p.add_argument("--tr", type=float, default=None, help="mean radiant temperature (C)")
306
+ p.add_argument("--v", type=float, default=1.0, help="wind speed at 10 m (m/s)")
307
+ p.add_argument("--rh", type=float, default=50.0, help="relative humidity (%%)")
308
+ p.set_defaults(func=_cmd_comfort)
309
+
310
+ p = sub.add_parser("osm", help="download OSM data and write a scene package")
311
+ p.add_argument("--lat", type=float, required=True)
312
+ p.add_argument("--lon", type=float, required=True)
313
+ p.add_argument("--half-size", type=float, default=250.0, help="half side of the square (m)")
314
+ p.add_argument("--cell", type=float, default=2.0)
315
+ p.add_argument("--tz", type=float, default=None)
316
+ p.add_argument("--title", default="OSM scene")
317
+ p.add_argument("--timeout", type=float, default=105.0)
318
+ p.add_argument("--out", required=True)
319
+ p.set_defaults(func=_cmd_osm)
320
+
321
+ p = sub.add_parser("demo", help="write a synthetic demo scene package")
322
+ p.add_argument("--out", required=True)
323
+ p.add_argument("--size", type=float, default=200.0)
324
+ p.add_argument("--cell", type=float, default=2.0)
325
+ p.set_defaults(func=_cmd_demo)
326
+
327
+ p = sub.add_parser("analyze", help="run a raster analysis on a scene package")
328
+ p.add_argument("scene", help="folder with scene.json")
329
+ p.add_argument("analysis", choices=ANALYSES)
330
+ p.add_argument("--out", default=None, help=".npy, .asc or .tif (rasterio) output")
331
+ p.add_argument("--date", type=_date, default=_dt.date(2026, 3, 21))
332
+ p.add_argument("--hour", type=float, default=12.0, help="clock hour for 'shadow'")
333
+ p.add_argument("--start", type=float, default=0.0)
334
+ p.add_argument("--end", type=float, default=24.0)
335
+ p.add_argument("--step", type=float, default=10.0, help="sun-path step (min)")
336
+ p.add_argument("--azimuths", type=int, default=36, help="horizon sectors for svf/irradiation")
337
+ p.add_argument("--trees", action="store_true", help="tree crowns block the sun")
338
+ p.add_argument("--x", type=float, default=None)
339
+ p.add_argument("--y", type=float, default=None)
340
+ p.add_argument("--radius", type=float, default=None)
341
+ p.add_argument("--level", type=float, default=None, help="water level for 'flood' (m)")
342
+ p.add_argument("--epw", default=None, help="EPW file for 'irradiation' (default: clear sky)")
343
+ p.set_defaults(func=_cmd_analyze)
344
+
345
+ p = sub.add_parser("view", help="open a scene package in the browser viewer")
346
+ p.add_argument("scene")
347
+ p.add_argument("--port", type=int, default=8640)
348
+ p.add_argument("--no-browser", action="store_true")
349
+ p.add_argument("--results", default=None, help="folder for results sent from the viewer")
350
+ p.set_defaults(func=_cmd_view)
351
+ return parser
352
+
353
+
354
+ def main(argv: Optional[Sequence[str]] = None) -> int:
355
+ parser = build_parser()
356
+ args = parser.parse_args(argv)
357
+ if getattr(args, "out", None) and args.command in ("osm", "demo"):
358
+ os.makedirs(args.out, exist_ok=True)
359
+ try:
360
+ return int(args.func(args) or 0)
361
+ except (OSError, ValueError, RuntimeError) as exc:
362
+ print(f"error: {exc}", file=sys.stderr)
363
+ return 1
364
+
365
+
366
+ if __name__ == "__main__": # pragma: no cover
367
+ sys.exit(main())