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
voxcity/utils/classes.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VoxCity Class Definitions
|
|
3
|
+
|
|
4
|
+
This module provides standard class definitions for voxel grid semantics
|
|
5
|
+
and land cover classification used throughout VoxCity.
|
|
6
|
+
|
|
7
|
+
Voxel Grid Semantics:
|
|
8
|
+
The 3D voxel grid uses integer codes to represent different urban elements.
|
|
9
|
+
Negative values represent structural elements, while positive values
|
|
10
|
+
represent land cover classes at the ground surface layer.
|
|
11
|
+
|
|
12
|
+
Land Cover Classes:
|
|
13
|
+
Land cover is standardized to a 1-based indexing system (1-14) for
|
|
14
|
+
consistency across different data sources. This standard classification
|
|
15
|
+
is used in the voxel representation and exports.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from typing import Dict, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Voxel Semantic Codes
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
VOXEL_CODES = {
|
|
27
|
+
-3: "Building",
|
|
28
|
+
-2: "Tree Canopy",
|
|
29
|
+
-1: "Ground/Subsurface",
|
|
30
|
+
# Positive values (>=1) represent land cover classes at ground surface
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
VOXEL_CODE_DESCRIPTIONS = """
|
|
34
|
+
Voxel Grid Semantic Codes:
|
|
35
|
+
-3 : Building volume
|
|
36
|
+
-2 : Tree canopy (vegetation)
|
|
37
|
+
-1 : Ground/Subsurface
|
|
38
|
+
>=1: Land cover class at ground surface (see Land Cover Classes)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Standard Land Cover Classes (1-based indices)
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
LAND_COVER_CLASSES = {
|
|
47
|
+
1: "Bareland",
|
|
48
|
+
2: "Rangeland",
|
|
49
|
+
3: "Shrub",
|
|
50
|
+
4: "Agriculture land",
|
|
51
|
+
5: "Tree",
|
|
52
|
+
6: "Moss and lichen",
|
|
53
|
+
7: "Wet land",
|
|
54
|
+
8: "Mangrove",
|
|
55
|
+
9: "Water",
|
|
56
|
+
10: "Snow and ice",
|
|
57
|
+
11: "Developed space",
|
|
58
|
+
12: "Road",
|
|
59
|
+
13: "Building",
|
|
60
|
+
14: "No Data",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
LAND_COVER_DESCRIPTIONS = """
|
|
64
|
+
VoxCity Standard Land Cover Classes (1-based indices, used in voxel grids):
|
|
65
|
+
--------------------------------------------------
|
|
66
|
+
1: Bareland - Bare soil, rocks, desert
|
|
67
|
+
2: Rangeland - Grassland, pasture
|
|
68
|
+
3: Shrub - Shrubland, bushes
|
|
69
|
+
4: Agriculture land - Cropland, farmland
|
|
70
|
+
5: Tree - Forest, tree cover
|
|
71
|
+
6: Moss and lichen - Moss, lichen cover
|
|
72
|
+
7: Wet land - Wetland, marsh
|
|
73
|
+
8: Mangrove - Mangrove forest
|
|
74
|
+
9: Water - Water bodies
|
|
75
|
+
10: Snow and ice - Snow, ice, glaciers
|
|
76
|
+
11: Developed space - Urban areas, parking
|
|
77
|
+
12: Road - Roads, paved surfaces
|
|
78
|
+
13: Building - Building footprints
|
|
79
|
+
14: No Data - Missing or invalid data
|
|
80
|
+
--------------------------------------------------
|
|
81
|
+
Note: Source-specific land cover classes are converted to these
|
|
82
|
+
standard classes during voxelization.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Print Helper Functions
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
def print_voxel_codes() -> None:
|
|
91
|
+
"""Print voxel semantic codes to console."""
|
|
92
|
+
print(VOXEL_CODE_DESCRIPTIONS)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def print_land_cover_classes() -> None:
|
|
96
|
+
"""Print standard land cover class definitions to console."""
|
|
97
|
+
print(LAND_COVER_DESCRIPTIONS)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_class_definitions() -> None:
|
|
101
|
+
"""Print both voxel codes and land cover class definitions."""
|
|
102
|
+
print("\n" + "=" * 60)
|
|
103
|
+
print("VoxCity Class Definitions")
|
|
104
|
+
print("=" * 60)
|
|
105
|
+
print(VOXEL_CODE_DESCRIPTIONS)
|
|
106
|
+
print(LAND_COVER_DESCRIPTIONS)
|
|
107
|
+
print("=" * 60 + "\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_land_cover_name(index: int) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Get the land cover class name for a given index.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
index: Land cover class index (1-14)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Class name string, or "Unknown" if index is invalid
|
|
119
|
+
"""
|
|
120
|
+
return LAND_COVER_CLASSES.get(index, "Unknown")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_voxel_code_name(code: int) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Get the semantic name for a voxel code.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
code: Voxel code (negative for structures, positive for land cover)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Semantic name string
|
|
132
|
+
"""
|
|
133
|
+
if code in VOXEL_CODES:
|
|
134
|
+
return VOXEL_CODES[code]
|
|
135
|
+
elif code >= 1:
|
|
136
|
+
return f"Land Cover: {get_land_cover_name(code)}"
|
|
137
|
+
elif code == 0:
|
|
138
|
+
return "Empty/Air"
|
|
139
|
+
else:
|
|
140
|
+
return "Unknown"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def summarize_voxel_grid(voxel_grid: np.ndarray, print_output: bool = True) -> Dict[int, int]:
|
|
144
|
+
"""
|
|
145
|
+
Summarize the contents of a voxel grid.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
voxel_grid: 3D numpy array of voxel codes
|
|
149
|
+
print_output: Whether to print the summary
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary mapping voxel codes to counts
|
|
153
|
+
"""
|
|
154
|
+
unique, counts = np.unique(voxel_grid, return_counts=True)
|
|
155
|
+
summary = dict(zip(unique.tolist(), counts.tolist()))
|
|
156
|
+
|
|
157
|
+
if print_output:
|
|
158
|
+
print("\nVoxel Grid Summary:")
|
|
159
|
+
print("-" * 40)
|
|
160
|
+
for code in sorted(summary.keys()):
|
|
161
|
+
name = get_voxel_code_name(code)
|
|
162
|
+
count = summary[code]
|
|
163
|
+
percentage = 100.0 * count / voxel_grid.size
|
|
164
|
+
print(f" {code:4d}: {name:25s} - {count:,} voxels ({percentage:.2f}%)")
|
|
165
|
+
print("-" * 40)
|
|
166
|
+
|
|
167
|
+
return summary
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def summarize_land_cover_grid(land_cover_grid: np.ndarray, print_output: bool = True) -> Dict[int, int]:
|
|
171
|
+
"""
|
|
172
|
+
Summarize the contents of a land cover grid.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
land_cover_grid: 2D numpy array of land cover class indices
|
|
176
|
+
print_output: Whether to print the summary
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dictionary mapping class indices to counts
|
|
180
|
+
"""
|
|
181
|
+
unique, counts = np.unique(land_cover_grid, return_counts=True)
|
|
182
|
+
summary = dict(zip(unique.tolist(), counts.tolist()))
|
|
183
|
+
|
|
184
|
+
if print_output:
|
|
185
|
+
print("\nLand Cover Grid Summary:")
|
|
186
|
+
print("-" * 40)
|
|
187
|
+
for idx in sorted(summary.keys()):
|
|
188
|
+
name = get_land_cover_name(idx) if idx >= 1 else f"Source-specific ({idx})"
|
|
189
|
+
count = summary[idx]
|
|
190
|
+
percentage = 100.0 * count / land_cover_grid.size
|
|
191
|
+
print(f" {idx:4d}: {name:25s} - {count:,} cells ({percentage:.2f}%)")
|
|
192
|
+
print("-" * 40)
|
|
193
|
+
|
|
194
|
+
return summary
|
voxcity/utils/lc.py
CHANGED
|
@@ -34,21 +34,22 @@ def rgb_distance(color1, color2):
|
|
|
34
34
|
return np.sqrt(np.sum((np.array(color1) - np.array(color2))**2))
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# Standard land cover classes mapping (1-based indices for voxel representation)
|
|
38
38
|
# land_cover_classes = {
|
|
39
|
-
# (128, 0, 0): 'Bareland',
|
|
40
|
-
# (0, 255, 36): 'Rangeland',
|
|
41
|
-
# (97, 140, 86): 'Shrub',
|
|
42
|
-
# (75, 181, 73): 'Agriculture land',
|
|
43
|
-
# (34, 97, 38): 'Tree',
|
|
44
|
-
# (
|
|
45
|
-
# (
|
|
46
|
-
# (
|
|
47
|
-
# (
|
|
48
|
-
# (
|
|
49
|
-
# (
|
|
50
|
-
# (
|
|
51
|
-
# (
|
|
39
|
+
# (128, 0, 0): 'Bareland', 1
|
|
40
|
+
# (0, 255, 36): 'Rangeland', 2
|
|
41
|
+
# (97, 140, 86): 'Shrub', 3
|
|
42
|
+
# (75, 181, 73): 'Agriculture land', 4
|
|
43
|
+
# (34, 97, 38): 'Tree', 5
|
|
44
|
+
# (255, 255, 0): 'Moss and lichen', 6
|
|
45
|
+
# (77, 118, 99): 'Wet land', 7
|
|
46
|
+
# (22, 61, 51): 'Mangrove', 8
|
|
47
|
+
# (0, 69, 255): 'Water', 9
|
|
48
|
+
# (205, 215, 224): 'Snow and ice', 10
|
|
49
|
+
# (148, 148, 148): 'Developed space', 11
|
|
50
|
+
# (255, 255, 255): 'Road', 12
|
|
51
|
+
# (222, 31, 7): 'Building', 13
|
|
52
|
+
# (128, 0, 0): 'No Data', 14
|
|
52
53
|
# }
|
|
53
54
|
|
|
54
55
|
def get_land_cover_classes(source):
|
|
@@ -161,22 +162,58 @@ def get_land_cover_classes(source):
|
|
|
161
162
|
}
|
|
162
163
|
return land_cover_classes
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
|
|
166
|
+
def get_source_class_descriptions(source):
|
|
167
|
+
"""
|
|
168
|
+
Get a formatted string describing land cover classes for a specific source.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
source (str): Name of the land cover data source.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
str: Formatted string describing the source's land cover classes.
|
|
175
|
+
"""
|
|
176
|
+
land_cover_classes = get_land_cover_classes(source)
|
|
177
|
+
# Get unique class names (values from the dict)
|
|
178
|
+
class_names = list(dict.fromkeys(land_cover_classes.values()))
|
|
179
|
+
|
|
180
|
+
lines = [f"\n{source} Land Cover Classes (source-specific, 0-based indices):"]
|
|
181
|
+
lines.append("-" * 55)
|
|
182
|
+
for idx, name in enumerate(class_names):
|
|
183
|
+
lines.append(f" {idx:2d}: {name}")
|
|
184
|
+
lines.append("-" * 55)
|
|
185
|
+
|
|
186
|
+
# Special note for OpenStreetMap/Standard which uses same class names
|
|
187
|
+
if source in ("OpenStreetMap", "Standard"):
|
|
188
|
+
lines.append("Note: OpenStreetMap uses VoxCity Standard class names.")
|
|
189
|
+
lines.append(" Indices shift from 0-based to 1-based during voxelization.")
|
|
190
|
+
else:
|
|
191
|
+
lines.append("Note: These source-specific classes will be converted to")
|
|
192
|
+
lines.append(" VoxCity Standard Classes (1-14) during voxelization.")
|
|
193
|
+
|
|
194
|
+
lines.append("")
|
|
195
|
+
lines.append("Access 2D land cover grid from VoxCity object:")
|
|
196
|
+
lines.append(" land_cover_grid = voxcity.land_cover.classes")
|
|
197
|
+
lines.append("")
|
|
198
|
+
return "\n".join(lines)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Standard land cover classes with numeric indices (1-based for voxel representation)
|
|
165
202
|
# land_cover_classes = {
|
|
166
|
-
# (128, 0, 0): 'Bareland',
|
|
167
|
-
# (0, 255, 36): 'Rangeland',
|
|
168
|
-
# (97, 140, 86): 'Shrub',
|
|
169
|
-
# (75, 181, 73): 'Agriculture land',
|
|
170
|
-
# (34, 97, 38): 'Tree',
|
|
171
|
-
# (
|
|
172
|
-
# (77, 118, 99): 'Wet land',
|
|
173
|
-
# (22, 61, 51): 'Mangrove',
|
|
174
|
-
# (0, 69, 255): 'Water',
|
|
175
|
-
# (205, 215, 224): 'Snow and ice',
|
|
176
|
-
# (148, 148, 148): 'Developed space',
|
|
177
|
-
# (255, 255, 255): 'Road',
|
|
178
|
-
# (222, 31, 7): 'Building',
|
|
179
|
-
# (128, 0, 0): 'No Data',
|
|
203
|
+
# (128, 0, 0): 'Bareland', 1
|
|
204
|
+
# (0, 255, 36): 'Rangeland', 2
|
|
205
|
+
# (97, 140, 86): 'Shrub', 3
|
|
206
|
+
# (75, 181, 73): 'Agriculture land', 4
|
|
207
|
+
# (34, 97, 38): 'Tree', 5
|
|
208
|
+
# (255, 255, 0): 'Moss and lichen', 6
|
|
209
|
+
# (77, 118, 99): 'Wet land', 7
|
|
210
|
+
# (22, 61, 51): 'Mangrove', 8
|
|
211
|
+
# (0, 69, 255): 'Water', 9
|
|
212
|
+
# (205, 215, 224): 'Snow and ice', 10
|
|
213
|
+
# (148, 148, 148): 'Developed space', 11
|
|
214
|
+
# (255, 255, 255): 'Road', 12
|
|
215
|
+
# (222, 31, 7): 'Building', 13
|
|
216
|
+
# (128, 0, 0): 'No Data', 14
|
|
180
217
|
# }
|
|
181
218
|
|
|
182
219
|
|
|
@@ -185,21 +222,23 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
185
222
|
"""
|
|
186
223
|
Optimized version using direct numpy array indexing instead of np.vectorize.
|
|
187
224
|
This is 10-100x faster than the original.
|
|
225
|
+
|
|
226
|
+
Returns 1-based class indices (1-14) for consistency with voxel representations.
|
|
188
227
|
"""
|
|
189
|
-
# Define mappings
|
|
228
|
+
# Define mappings (1-based indices: 1=Bareland, 2=Rangeland, ..., 14=No Data)
|
|
190
229
|
if land_cover_source == 'Urbanwatch':
|
|
191
|
-
mapping = {0:
|
|
230
|
+
mapping = {0: 13, 1: 12, 2: 11, 3: 5, 4: 2, 5: 4, 6: 9, 7: 1, 8: 14, 9: 9}
|
|
192
231
|
elif land_cover_source == 'ESA WorldCover':
|
|
193
|
-
mapping = {0:
|
|
232
|
+
mapping = {0: 5, 1: 3, 2: 2, 3: 4, 4: 11, 5: 1, 6: 10, 7: 9, 8: 7, 9: 8, 10: 6}
|
|
194
233
|
elif land_cover_source == "ESRI 10m Annual Land Cover":
|
|
195
|
-
mapping = {0:
|
|
234
|
+
mapping = {0: 14, 1: 9, 2: 5, 3: 2, 4: 7, 5: 4, 6: 3, 7: 11, 8: 1, 9: 10, 10: 14}
|
|
196
235
|
elif land_cover_source == "Dynamic World V1":
|
|
197
|
-
mapping = {0:
|
|
236
|
+
mapping = {0: 9, 1: 5, 2: 2, 3: 7, 4: 4, 5: 3, 6: 11, 7: 1, 8: 10}
|
|
198
237
|
elif land_cover_source == "OpenEarthMapJapan":
|
|
199
|
-
mapping = {0:
|
|
238
|
+
mapping = {0: 1, 1: 2, 2: 11, 3: 12, 4: 5, 5: 9, 6: 4, 7: 13}
|
|
200
239
|
else:
|
|
201
|
-
# If unknown source, return as-is
|
|
202
|
-
return input_array.copy()
|
|
240
|
+
# If unknown source, return as-is with +1 offset for consistency
|
|
241
|
+
return input_array.copy() + 1
|
|
203
242
|
|
|
204
243
|
# Create a full mapping array for all possible values (0-255 for uint8)
|
|
205
244
|
max_val = max(max(mapping.keys()), input_array.max()) + 1
|
|
@@ -374,12 +413,14 @@ def convert_land_cover_array(input_array, land_cover_classes):
|
|
|
374
413
|
land_cover_classes (dict): Dictionary mapping RGB tuples to class names
|
|
375
414
|
|
|
376
415
|
Returns:
|
|
377
|
-
numpy.ndarray: Array with integer indices corresponding to land cover classes
|
|
416
|
+
numpy.ndarray: Array with 0-based integer indices corresponding to land cover classes
|
|
378
417
|
|
|
379
418
|
Note:
|
|
380
419
|
Classes not found in the mapping are assigned index -1
|
|
420
|
+
Indices are 0-based as source-specific indices. Use convert_land_cover() to
|
|
421
|
+
remap to standard 1-based indices for voxel representation.
|
|
381
422
|
"""
|
|
382
|
-
# Create a mapping of class names to integers
|
|
423
|
+
# Create a mapping of class names to integers (0-based, source-specific order)
|
|
383
424
|
class_to_int = {name: i for i, name in enumerate(land_cover_classes.values())}
|
|
384
425
|
|
|
385
426
|
# Create a vectorized function to map string values to integers
|
voxcity/utils/logging.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight, centralized logging utilities for the voxcity package.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from voxcity.utils.logging import get_logger
|
|
6
|
+
logger = get_logger(__name__)
|
|
7
|
+
|
|
8
|
+
Environment variables:
|
|
9
|
+
VOXCITY_LOG_LEVEL: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_LEVEL_NAMES = {
|
|
20
|
+
"CRITICAL": logging.CRITICAL,
|
|
21
|
+
"ERROR": logging.ERROR,
|
|
22
|
+
"WARNING": logging.WARNING,
|
|
23
|
+
"INFO": logging.INFO,
|
|
24
|
+
"DEBUG": logging.DEBUG,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _resolve_level(env_value: Optional[str]) -> int:
|
|
29
|
+
if not env_value:
|
|
30
|
+
return logging.INFO
|
|
31
|
+
return _LEVEL_NAMES.get(env_value.strip().upper(), logging.INFO)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _configure_root_once() -> None:
|
|
35
|
+
root = logging.getLogger("voxcity")
|
|
36
|
+
if root.handlers:
|
|
37
|
+
return
|
|
38
|
+
level = _resolve_level(os.getenv("VOXCITY_LOG_LEVEL"))
|
|
39
|
+
root.setLevel(level)
|
|
40
|
+
handler = logging.StreamHandler()
|
|
41
|
+
handler.setLevel(level)
|
|
42
|
+
formatter = logging.Formatter(
|
|
43
|
+
fmt="%(levelname)s | %(name)s | %(message)s",
|
|
44
|
+
)
|
|
45
|
+
handler.setFormatter(formatter)
|
|
46
|
+
root.addHandler(handler)
|
|
47
|
+
# Prevent duplicate messages from propagating to the global root logger
|
|
48
|
+
root.propagate = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
52
|
+
"""Return a child logger under the package root logger.
|
|
53
|
+
|
|
54
|
+
- Ensures a single configuration for the package
|
|
55
|
+
- Respects VOXCITY_LOG_LEVEL if set
|
|
56
|
+
"""
|
|
57
|
+
_configure_root_once()
|
|
58
|
+
pkg_logger = logging.getLogger("voxcity")
|
|
59
|
+
return pkg_logger.getChild(name) if name else pkg_logger
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Grid orientation helpers.
|
|
2
|
+
|
|
3
|
+
Contract:
|
|
4
|
+
- Canonical internal orientation is "north_up": row 0 is the northern/top row,
|
|
5
|
+
increasing row index moves south/down. Columns increase eastward: column 0 is
|
|
6
|
+
west/left and indices increase toward the east/right. All processing functions
|
|
7
|
+
accept and return 2D grids in this orientation unless explicitly documented
|
|
8
|
+
otherwise.
|
|
9
|
+
- Visualization utilities may flip vertically for display purposes only.
|
|
10
|
+
- 3D indexing follows (row, col, z) = (north→south, west→east, ground→up).
|
|
11
|
+
|
|
12
|
+
Utilities here are intentionally minimal to avoid introducing hidden behavior.
|
|
13
|
+
They can be used at I/O boundaries (e.g., when reading rasters with south_up
|
|
14
|
+
conventions) to normalize to the internal orientation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Literal
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
# Public constants to reference orientation in docs and code
|
|
23
|
+
ORIENTATION_NORTH_UP: Literal["north_up"] = "north_up"
|
|
24
|
+
ORIENTATION_SOUTH_UP: Literal["south_up"] = "south_up"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def ensure_orientation(
|
|
28
|
+
grid: np.ndarray,
|
|
29
|
+
orientation_in: Literal["north_up", "south_up"],
|
|
30
|
+
orientation_out: Literal["north_up", "south_up"] = ORIENTATION_NORTH_UP,
|
|
31
|
+
) -> np.ndarray:
|
|
32
|
+
"""Return ``grid`` converted from ``orientation_in`` to ``orientation_out``.
|
|
33
|
+
|
|
34
|
+
Both orientations are defined for 2D arrays as:
|
|
35
|
+
- north_up: row 0 = north/top, last row = south/bottom
|
|
36
|
+
- south_up: row 0 = south/bottom, last row = north/top
|
|
37
|
+
|
|
38
|
+
If orientations match, the input array is returned unchanged. When converting
|
|
39
|
+
between north_up and south_up, a vertical flip is applied using ``np.flipud``.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
- This function does not copy when no conversion is needed.
|
|
44
|
+
- Use at data boundaries (read/write, interop) rather than deep in processing code.
|
|
45
|
+
"""
|
|
46
|
+
if orientation_in == orientation_out:
|
|
47
|
+
return grid
|
|
48
|
+
# Only two orientations supported; converting between them is a vertical flip
|
|
49
|
+
return np.flipud(grid)
|
|
50
|
+
|
|
51
|
+
|