voxcity 0.6.26__py3-none-any.whl → 1.0.2__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.
- voxcity/__init__.py +10 -4
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +9 -1
- voxcity/exporter/cityles.py +129 -34
- voxcity/exporter/envimet.py +51 -26
- voxcity/exporter/magicavoxel.py +42 -5
- voxcity/exporter/netcdf.py +27 -0
- voxcity/exporter/obj.py +103 -28
- voxcity/generator/__init__.py +47 -0
- voxcity/generator/api.py +721 -0
- voxcity/generator/grids.py +381 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +392 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +1488 -1169
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +26 -10
- voxcity/geoprocessor/network.py +35 -6
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +435 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +159 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +824 -820
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +66 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/shape.py +230 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +1145 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
- voxcity-1.0.2.dist-info/RECORD +81 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from ..utils.logging import get_logger
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..models import (
|
|
7
|
+
GridMetadata,
|
|
8
|
+
BuildingGrid,
|
|
9
|
+
LandCoverGrid,
|
|
10
|
+
DemGrid,
|
|
11
|
+
VoxelGrid,
|
|
12
|
+
CanopyGrid,
|
|
13
|
+
VoxCity,
|
|
14
|
+
PipelineConfig,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .grids import (
|
|
18
|
+
get_land_cover_grid,
|
|
19
|
+
get_building_height_grid,
|
|
20
|
+
get_canopy_height_grid,
|
|
21
|
+
get_dem_grid,
|
|
22
|
+
)
|
|
23
|
+
from .voxelizer import Voxelizer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VoxCityPipeline:
|
|
27
|
+
def __init__(self, meshsize: float, rectangle_vertices, crs: str = "EPSG:4326") -> None:
|
|
28
|
+
self.meshsize = float(meshsize)
|
|
29
|
+
self.rectangle_vertices = rectangle_vertices
|
|
30
|
+
self.crs = crs
|
|
31
|
+
|
|
32
|
+
def _bounds(self):
|
|
33
|
+
xs = [p[0] for p in self.rectangle_vertices]
|
|
34
|
+
ys = [p[1] for p in self.rectangle_vertices]
|
|
35
|
+
return (min(xs), min(ys), max(xs), max(ys))
|
|
36
|
+
|
|
37
|
+
def _meta(self) -> GridMetadata:
|
|
38
|
+
return GridMetadata(crs=self.crs, bounds=self._bounds(), meshsize=self.meshsize)
|
|
39
|
+
|
|
40
|
+
def assemble_voxcity(
|
|
41
|
+
self,
|
|
42
|
+
voxcity_grid: np.ndarray,
|
|
43
|
+
building_height_grid: np.ndarray,
|
|
44
|
+
building_min_height_grid: np.ndarray,
|
|
45
|
+
building_id_grid: np.ndarray,
|
|
46
|
+
land_cover_grid: np.ndarray,
|
|
47
|
+
dem_grid: np.ndarray,
|
|
48
|
+
canopy_height_top: Optional[np.ndarray] = None,
|
|
49
|
+
canopy_height_bottom: Optional[np.ndarray] = None,
|
|
50
|
+
extras: Optional[dict] = None,
|
|
51
|
+
) -> VoxCity:
|
|
52
|
+
meta = self._meta()
|
|
53
|
+
buildings = BuildingGrid(
|
|
54
|
+
heights=building_height_grid,
|
|
55
|
+
min_heights=building_min_height_grid,
|
|
56
|
+
ids=building_id_grid,
|
|
57
|
+
meta=meta,
|
|
58
|
+
)
|
|
59
|
+
land = LandCoverGrid(classes=land_cover_grid, meta=meta)
|
|
60
|
+
dem = DemGrid(elevation=dem_grid, meta=meta)
|
|
61
|
+
voxels = VoxelGrid(classes=voxcity_grid, meta=meta)
|
|
62
|
+
canopy = CanopyGrid(top=canopy_height_top if canopy_height_top is not None else np.zeros_like(land_cover_grid, dtype=float),
|
|
63
|
+
bottom=canopy_height_bottom,
|
|
64
|
+
meta=meta)
|
|
65
|
+
_extras = {
|
|
66
|
+
"rectangle_vertices": self.rectangle_vertices,
|
|
67
|
+
"canopy_top": canopy.top,
|
|
68
|
+
"canopy_bottom": canopy.bottom,
|
|
69
|
+
}
|
|
70
|
+
if extras:
|
|
71
|
+
_extras.update(extras)
|
|
72
|
+
return VoxCity(voxels=voxels, buildings=buildings, land_cover=land, dem=dem, tree_canopy=canopy, extras=_extras)
|
|
73
|
+
|
|
74
|
+
def run(self, cfg: PipelineConfig, building_gdf=None, terrain_gdf=None, **kwargs) -> VoxCity:
|
|
75
|
+
os.makedirs(cfg.output_dir, exist_ok=True)
|
|
76
|
+
land_strategy = LandCoverSourceFactory.create(cfg.land_cover_source)
|
|
77
|
+
build_strategy = BuildingSourceFactory.create(cfg.building_source)
|
|
78
|
+
canopy_strategy = CanopySourceFactory.create(cfg.canopy_height_source, cfg)
|
|
79
|
+
dem_strategy = DemSourceFactory.create(cfg.dem_source)
|
|
80
|
+
|
|
81
|
+
# Prefer structured options from cfg; allow legacy kwargs for back-compat
|
|
82
|
+
land_cover_grid = land_strategy.build_grid(
|
|
83
|
+
cfg.rectangle_vertices, cfg.meshsize, cfg.output_dir,
|
|
84
|
+
**{**cfg.land_cover_options, **kwargs}
|
|
85
|
+
)
|
|
86
|
+
# Detect effective land cover source (e.g., Urbanwatch -> OpenStreetMap fallback)
|
|
87
|
+
try:
|
|
88
|
+
from .grids import get_last_effective_land_cover_source
|
|
89
|
+
lc_src_effective = get_last_effective_land_cover_source() or cfg.land_cover_source
|
|
90
|
+
except Exception:
|
|
91
|
+
lc_src_effective = cfg.land_cover_source
|
|
92
|
+
bh, bmin, bid, building_gdf_out = build_strategy.build_grids(
|
|
93
|
+
cfg.rectangle_vertices, cfg.meshsize, cfg.output_dir,
|
|
94
|
+
building_gdf=building_gdf,
|
|
95
|
+
**{**cfg.building_options, **kwargs}
|
|
96
|
+
)
|
|
97
|
+
canopy_top, canopy_bottom = canopy_strategy.build_grids(
|
|
98
|
+
cfg.rectangle_vertices, cfg.meshsize, land_cover_grid, cfg.output_dir,
|
|
99
|
+
land_cover_source=lc_src_effective,
|
|
100
|
+
**{**cfg.canopy_options, **kwargs}
|
|
101
|
+
)
|
|
102
|
+
dem = dem_strategy.build_grid(
|
|
103
|
+
cfg.rectangle_vertices, cfg.meshsize, land_cover_grid, cfg.output_dir,
|
|
104
|
+
terrain_gdf=terrain_gdf,
|
|
105
|
+
land_cover_like=land_cover_grid,
|
|
106
|
+
**{**cfg.dem_options, **kwargs}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
ro = cfg.remove_perimeter_object
|
|
110
|
+
if (ro is not None) and (ro > 0):
|
|
111
|
+
w_peri = int(ro * bh.shape[0] + 0.5)
|
|
112
|
+
h_peri = int(ro * bh.shape[1] + 0.5)
|
|
113
|
+
canopy_top[:w_peri, :] = canopy_top[-w_peri:, :] = canopy_top[:, :h_peri] = canopy_top[:, -h_peri:] = 0
|
|
114
|
+
canopy_bottom[:w_peri, :] = canopy_bottom[-w_peri:, :] = canopy_bottom[:, :h_peri] = canopy_bottom[:, -h_peri:] = 0
|
|
115
|
+
ids1 = np.unique(bid[:w_peri, :][bid[:w_peri, :] > 0]); ids2 = np.unique(bid[-w_peri:, :][bid[-w_peri:, :] > 0])
|
|
116
|
+
ids3 = np.unique(bid[:, :h_peri][bid[:, :h_peri] > 0]); ids4 = np.unique(bid[:, -h_peri:][bid[:, -h_peri:] > 0])
|
|
117
|
+
for rid in np.concatenate((ids1, ids2, ids3, ids4)):
|
|
118
|
+
pos = np.where(bid == rid)
|
|
119
|
+
bh[pos] = 0
|
|
120
|
+
bmin[pos] = [[] for _ in range(len(bmin[pos]))]
|
|
121
|
+
|
|
122
|
+
voxelizer = Voxelizer(
|
|
123
|
+
voxel_size=cfg.meshsize,
|
|
124
|
+
land_cover_source=lc_src_effective,
|
|
125
|
+
trunk_height_ratio=cfg.trunk_height_ratio,
|
|
126
|
+
voxel_dtype=kwargs.get("voxel_dtype", np.int8),
|
|
127
|
+
max_voxel_ram_mb=kwargs.get("max_voxel_ram_mb"),
|
|
128
|
+
)
|
|
129
|
+
vox = voxelizer.generate_combined(
|
|
130
|
+
building_height_grid_ori=bh,
|
|
131
|
+
building_min_height_grid_ori=bmin,
|
|
132
|
+
building_id_grid_ori=bid,
|
|
133
|
+
land_cover_grid_ori=land_cover_grid,
|
|
134
|
+
dem_grid_ori=dem,
|
|
135
|
+
tree_grid_ori=canopy_top,
|
|
136
|
+
canopy_bottom_height_grid_ori=canopy_bottom,
|
|
137
|
+
)
|
|
138
|
+
return self.assemble_voxcity(
|
|
139
|
+
voxcity_grid=vox,
|
|
140
|
+
building_height_grid=bh,
|
|
141
|
+
building_min_height_grid=bmin,
|
|
142
|
+
building_id_grid=bid,
|
|
143
|
+
land_cover_grid=land_cover_grid,
|
|
144
|
+
dem_grid=dem,
|
|
145
|
+
canopy_height_top=canopy_top,
|
|
146
|
+
canopy_height_bottom=canopy_bottom,
|
|
147
|
+
extras={
|
|
148
|
+
"building_gdf": building_gdf_out,
|
|
149
|
+
"land_cover_source": lc_src_effective,
|
|
150
|
+
"building_source": cfg.building_source,
|
|
151
|
+
"dem_source": cfg.dem_source,
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LandCoverSourceStrategy: # ABC simplified to avoid dependency in split
|
|
157
|
+
def build_grid(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs): # pragma: no cover - interface
|
|
158
|
+
raise NotImplementedError
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DefaultLandCoverStrategy(LandCoverSourceStrategy):
|
|
162
|
+
def __init__(self, source: str) -> None:
|
|
163
|
+
self.source = source
|
|
164
|
+
|
|
165
|
+
def build_grid(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs):
|
|
166
|
+
return get_land_cover_grid(rectangle_vertices, meshsize, self.source, output_dir, **kwargs)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class LandCoverSourceFactory:
|
|
170
|
+
@staticmethod
|
|
171
|
+
def create(source: str) -> LandCoverSourceStrategy:
|
|
172
|
+
return DefaultLandCoverStrategy(source)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class BuildingSourceStrategy: # ABC simplified
|
|
176
|
+
def build_grids(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs): # pragma: no cover - interface
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class DefaultBuildingSourceStrategy(BuildingSourceStrategy):
|
|
181
|
+
def __init__(self, source: str) -> None:
|
|
182
|
+
self.source = source
|
|
183
|
+
|
|
184
|
+
def build_grids(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs):
|
|
185
|
+
return get_building_height_grid(rectangle_vertices, meshsize, self.source, output_dir, **kwargs)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class BuildingSourceFactory:
|
|
189
|
+
@staticmethod
|
|
190
|
+
def create(source: str) -> BuildingSourceStrategy:
|
|
191
|
+
return DefaultBuildingSourceStrategy(source)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class CanopySourceStrategy: # ABC simplified
|
|
195
|
+
def build_grids(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs): # pragma: no cover
|
|
196
|
+
raise NotImplementedError
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class StaticCanopyStrategy(CanopySourceStrategy):
|
|
200
|
+
def __init__(self, cfg: PipelineConfig) -> None:
|
|
201
|
+
self.cfg = cfg
|
|
202
|
+
|
|
203
|
+
def build_grids(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
|
|
204
|
+
canopy_top = np.zeros_like(land_cover_grid, dtype=float)
|
|
205
|
+
static_h = self.cfg.static_tree_height if self.cfg.static_tree_height is not None else kwargs.get("static_tree_height", 10.0)
|
|
206
|
+
from ..utils.lc import get_land_cover_classes
|
|
207
|
+
_classes = get_land_cover_classes(self.cfg.land_cover_source)
|
|
208
|
+
_class_to_int = {name: i for i, name in enumerate(_classes.values())}
|
|
209
|
+
_tree_labels = ["Tree", "Trees", "Tree Canopy"]
|
|
210
|
+
_tree_idx = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
|
|
211
|
+
tree_mask = np.isin(land_cover_grid, _tree_idx) if _tree_idx else np.zeros_like(land_cover_grid, dtype=bool)
|
|
212
|
+
canopy_top[tree_mask] = static_h
|
|
213
|
+
tr = self.cfg.trunk_height_ratio if self.cfg.trunk_height_ratio is not None else (11.76 / 19.98)
|
|
214
|
+
canopy_bottom = canopy_top * float(tr)
|
|
215
|
+
return canopy_top, canopy_bottom
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SourceCanopyStrategy(CanopySourceStrategy):
|
|
219
|
+
def __init__(self, source: str) -> None:
|
|
220
|
+
self.source = source
|
|
221
|
+
|
|
222
|
+
def build_grids(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
|
|
223
|
+
# Provide land_cover_like for graceful fallback sizing without EE
|
|
224
|
+
return get_canopy_height_grid(
|
|
225
|
+
rectangle_vertices,
|
|
226
|
+
meshsize,
|
|
227
|
+
self.source,
|
|
228
|
+
output_dir,
|
|
229
|
+
land_cover_like=land_cover_grid,
|
|
230
|
+
**kwargs,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CanopySourceFactory:
|
|
235
|
+
@staticmethod
|
|
236
|
+
def create(source: str, cfg: PipelineConfig) -> CanopySourceStrategy:
|
|
237
|
+
if source == "Static":
|
|
238
|
+
return StaticCanopyStrategy(cfg)
|
|
239
|
+
return SourceCanopyStrategy(source)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class DemSourceStrategy: # ABC simplified
|
|
243
|
+
def build_grid(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs): # pragma: no cover
|
|
244
|
+
raise NotImplementedError
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class FlatDemStrategy(DemSourceStrategy):
|
|
248
|
+
def build_grid(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
|
|
249
|
+
return np.zeros_like(land_cover_grid)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class SourceDemStrategy(DemSourceStrategy):
|
|
253
|
+
def __init__(self, source: str) -> None:
|
|
254
|
+
self.source = source
|
|
255
|
+
|
|
256
|
+
def build_grid(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
|
|
257
|
+
terrain_gdf = kwargs.get("terrain_gdf")
|
|
258
|
+
if terrain_gdf is not None:
|
|
259
|
+
from ..geoprocessor.raster import create_dem_grid_from_gdf_polygon
|
|
260
|
+
return create_dem_grid_from_gdf_polygon(terrain_gdf, meshsize, rectangle_vertices)
|
|
261
|
+
try:
|
|
262
|
+
return get_dem_grid(rectangle_vertices, meshsize, self.source, output_dir, **kwargs)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
# Fallback to flat DEM if source fails or unsupported
|
|
265
|
+
logger = get_logger(__name__)
|
|
266
|
+
logger.warning("DEM source '%s' failed (%s). Falling back to flat DEM.", self.source, e)
|
|
267
|
+
return np.zeros_like(land_cover_grid)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class DemSourceFactory:
|
|
271
|
+
@staticmethod
|
|
272
|
+
def create(source: str) -> DemSourceStrategy:
|
|
273
|
+
# Normalize and auto-fallback: None/"none" -> Flat
|
|
274
|
+
try:
|
|
275
|
+
src_norm = (source or "").strip().lower()
|
|
276
|
+
except Exception:
|
|
277
|
+
src_norm = ""
|
|
278
|
+
if (not source) or (src_norm in {"flat", "none", "null"}):
|
|
279
|
+
return FlatDemStrategy()
|
|
280
|
+
return SourceDemStrategy(source)
|
|
281
|
+
|
|
282
|
+
|