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.
- zero2geospatial3d/__init__.py +105 -0
- zero2geospatial3d/acoustics.py +284 -0
- zero2geospatial3d/classify.py +379 -0
- zero2geospatial3d/cli.py +367 -0
- zero2geospatial3d/comfort.py +687 -0
- zero2geospatial3d/data/epw_stations.json.gz +0 -0
- zero2geospatial3d/daylight.py +337 -0
- zero2geospatial3d/dispersion.py +249 -0
- zero2geospatial3d/epw.py +162 -0
- zero2geospatial3d/geo.py +138 -0
- zero2geospatial3d/grid.py +119 -0
- zero2geospatial3d/microclimate.py +388 -0
- zero2geospatial3d/morphology.py +283 -0
- zero2geospatial3d/nsga2.py +315 -0
- zero2geospatial3d/osm.py +450 -0
- zero2geospatial3d/osm_tags.py +180 -0
- zero2geospatial3d/py.typed +0 -0
- zero2geospatial3d/results.py +231 -0
- zero2geospatial3d/road_sources.py +141 -0
- zero2geospatial3d/scene.py +555 -0
- zero2geospatial3d/server.py +332 -0
- zero2geospatial3d/shading.py +443 -0
- zero2geospatial3d/solar.py +207 -0
- zero2geospatial3d/terrain.py +610 -0
- zero2geospatial3d/urban_form.py +326 -0
- zero2geospatial3d/walkability.py +380 -0
- zero2geospatial3d/weather.py +373 -0
- zero2geospatial3d/web/index.html +50 -0
- zero2geospatial3d/web/src/analysis/climate.js +278 -0
- zero2geospatial3d/web/src/analysis/compliance.js +345 -0
- zero2geospatial3d/web/src/analysis/gpu.js +431 -0
- zero2geospatial3d/web/src/analysis/optimize.js +208 -0
- zero2geospatial3d/web/src/analysis/runners.js +1644 -0
- zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
- zero2geospatial3d/web/src/app.js +630 -0
- zero2geospatial3d/web/src/core/acoustics.js +341 -0
- zero2geospatial3d/web/src/core/classify.js +158 -0
- zero2geospatial3d/web/src/core/comfort.js +686 -0
- zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
- zero2geospatial3d/web/src/core/dispersion.js +264 -0
- zero2geospatial3d/web/src/core/microclimate.js +372 -0
- zero2geospatial3d/web/src/core/morphology.js +153 -0
- zero2geospatial3d/web/src/core/nsga2.js +181 -0
- zero2geospatial3d/web/src/core/party_walls.js +60 -0
- zero2geospatial3d/web/src/core/road_sources.js +127 -0
- zero2geospatial3d/web/src/core/solar.js +112 -0
- zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
- zero2geospatial3d/web/src/core/terrain_math.js +279 -0
- zero2geospatial3d/web/src/core/urban_form.js +405 -0
- zero2geospatial3d/web/src/core/walkability.js +316 -0
- zero2geospatial3d/web/src/core/weather.js +225 -0
- zero2geospatial3d/web/src/core/wind.js +131 -0
- zero2geospatial3d/web/src/export/check_pdf.js +74 -0
- zero2geospatial3d/web/src/export/exporters.js +556 -0
- zero2geospatial3d/web/src/export/pdf.js +149 -0
- zero2geospatial3d/web/src/export/snapshot.js +234 -0
- zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
- zero2geospatial3d/web/src/main.js +75 -0
- zero2geospatial3d/web/src/scene/objects.js +645 -0
- zero2geospatial3d/web/src/scene/overlay.js +145 -0
- zero2geospatial3d/web/src/scene/terrain.js +546 -0
- zero2geospatial3d/web/src/scene/viewer.js +275 -0
- zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
- zero2geospatial3d/web/src/style.css +225 -0
- zero2geospatial3d/web/src/ui/building_report.js +404 -0
- zero2geospatial3d/web/src/ui/charts.js +441 -0
- zero2geospatial3d/web/src/ui/colorize.js +101 -0
- zero2geospatial3d/web/src/ui/compare.js +362 -0
- zero2geospatial3d/web/src/ui/dom.js +104 -0
- zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
- zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
- zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
- zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
- zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
- zero2geospatial3d/web/src/ui/sunclock.js +45 -0
- zero2geospatial3d/web/src/ui/tools.js +197 -0
- zero2geospatial3d/web/src/ui/viewtools.js +173 -0
- zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
- zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
- zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
- zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
- zero2geospatial3d/wind.py +178 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
zero2geospatial3d/cli.py
ADDED
|
@@ -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())
|