voxcity 0.6.2__py3-none-any.whl → 0.6.3__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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/exporter/cityles.py +276 -141
- voxcity/generator.py +1073 -1073
- voxcity/geoprocessor/mesh.py +790 -790
- voxcity/simulator/view.py +2238 -2238
- {voxcity-0.6.2.dist-info → voxcity-0.6.3.dist-info}/METADATA +1 -1
- {voxcity-0.6.2.dist-info → voxcity-0.6.3.dist-info}/RECORD +10 -10
- {voxcity-0.6.2.dist-info → voxcity-0.6.3.dist-info}/WHEEL +0 -0
- {voxcity-0.6.2.dist-info → voxcity-0.6.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.2.dist-info → voxcity-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.6.2.dist-info → voxcity-0.6.3.dist-info}/top_level.txt +0 -0
voxcity/exporter/cityles.py
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
|
-
CityLES
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Generates all required text files and metadata for CityLES runs
|
|
13
|
-
|
|
14
|
-
Main Functions:
|
|
15
|
-
- export_cityles: Main function to export all required CityLES input files
|
|
16
|
-
- export_topog: Exports building geometry (topog.txt)
|
|
17
|
-
- export_landuse: Exports land use grid (landuse.txt)
|
|
18
|
-
- export_dem: Exports digital elevation model (dem.txt)
|
|
19
|
-
- export_vmap: Exports vegetation map (vmap.txt)
|
|
20
|
-
- export_lonlat: Exports longitude/latitude grid (lonlat.txt)
|
|
21
|
-
|
|
22
|
-
Dependencies:
|
|
23
|
-
- numpy: For array operations
|
|
24
|
-
- pathlib: For file and directory management
|
|
25
|
-
- os: For file system operations
|
|
2
|
+
CityLES export module for VoxCity
|
|
3
|
+
Exports VoxCity grid data to CityLES input file format
|
|
4
|
+
Updated 2025/08/05 with corrected land use and building material codes
|
|
5
|
+
Integrated with VoxCity land cover utilities
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- This module expects raw land cover grids as produced per-source by VoxCity, not
|
|
9
|
+
standardized/converted indices. Supported sources:
|
|
10
|
+
'OpenStreetMap', 'Urbanwatch', 'OpenEarthMapJapan', 'ESA WorldCover',
|
|
11
|
+
'ESRI 10m Annual Land Cover', 'Dynamic World V1'.
|
|
26
12
|
"""
|
|
27
13
|
|
|
28
14
|
import os
|
|
@@ -30,47 +16,134 @@ import numpy as np
|
|
|
30
16
|
from pathlib import Path
|
|
31
17
|
|
|
32
18
|
|
|
33
|
-
#
|
|
34
|
-
# Based on
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
19
|
+
# VoxCity standard land cover classes after conversion
|
|
20
|
+
# Based on convert_land_cover function output
|
|
21
|
+
VOXCITY_STANDARD_CLASSES = {
|
|
22
|
+
0: 'Bareland',
|
|
23
|
+
1: 'Rangeland',
|
|
24
|
+
2: 'Shrub',
|
|
25
|
+
3: 'Agriculture land',
|
|
26
|
+
4: 'Tree',
|
|
27
|
+
5: 'Moss and lichen',
|
|
28
|
+
6: 'Wet land',
|
|
29
|
+
7: 'Mangrove',
|
|
30
|
+
8: 'Water',
|
|
31
|
+
9: 'Snow and ice',
|
|
32
|
+
10: 'Developed space',
|
|
33
|
+
11: 'Road',
|
|
34
|
+
12: 'Building',
|
|
35
|
+
13: 'No Data'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
## Source-specific class name to CityLES land use mappings
|
|
39
|
+
# CityLES land use codes: 1=Water, 2=Rice Paddy, 3=Crops, 4=Grassland, 5=Deciduous Broadleaf Forest,
|
|
40
|
+
# 9=Bare Land, 10=Building, 16=Asphalt (road), etc.
|
|
41
|
+
|
|
42
|
+
# OpenStreetMap / Standard
|
|
43
|
+
OSM_CLASS_TO_CITYLES = {
|
|
44
|
+
'Bareland': 9,
|
|
45
|
+
'Rangeland': 4,
|
|
46
|
+
'Shrub': 4,
|
|
47
|
+
'Moss and lichen': 4,
|
|
48
|
+
'Agriculture land': 3,
|
|
49
|
+
'Tree': 5,
|
|
50
|
+
'Wet land': 2,
|
|
51
|
+
'Mangroves': 5,
|
|
52
|
+
'Water': 1,
|
|
53
|
+
'Snow and ice': 9,
|
|
54
|
+
'Developed space': 10,
|
|
55
|
+
'Road': 16,
|
|
56
|
+
'Building': 10,
|
|
57
|
+
'No Data': 4
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
#
|
|
60
|
-
|
|
60
|
+
# Urbanwatch
|
|
61
|
+
URBANWATCH_CLASS_TO_CITYLES = {
|
|
62
|
+
'Building': 10,
|
|
63
|
+
'Road': 16,
|
|
64
|
+
'Parking Lot': 16,
|
|
65
|
+
'Tree Canopy': 5,
|
|
66
|
+
'Grass/Shrub': 4,
|
|
67
|
+
'Agriculture': 3,
|
|
68
|
+
'Water': 1,
|
|
69
|
+
'Barren': 9,
|
|
70
|
+
'Unknown': 4,
|
|
71
|
+
'Sea': 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# OpenEarthMapJapan
|
|
75
|
+
OEMJ_CLASS_TO_CITYLES = {
|
|
76
|
+
'Bareland': 9,
|
|
77
|
+
'Rangeland': 4,
|
|
78
|
+
'Developed space': 10,
|
|
79
|
+
'Road': 16,
|
|
80
|
+
'Tree': 5,
|
|
81
|
+
'Water': 1,
|
|
82
|
+
'Agriculture land': 3,
|
|
83
|
+
'Building': 10
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ESA WorldCover
|
|
87
|
+
ESA_CLASS_TO_CITYLES = {
|
|
88
|
+
'Trees': 5,
|
|
89
|
+
'Shrubland': 4,
|
|
90
|
+
'Grassland': 4,
|
|
91
|
+
'Cropland': 3,
|
|
92
|
+
'Built-up': 10,
|
|
93
|
+
'Barren / sparse vegetation': 9,
|
|
94
|
+
'Snow and ice': 9,
|
|
95
|
+
'Open water': 1,
|
|
96
|
+
'Herbaceous wetland': 2,
|
|
97
|
+
'Mangroves': 5,
|
|
98
|
+
'Moss and lichen': 9
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ESRI 10m Annual Land Cover
|
|
102
|
+
ESRI_CLASS_TO_CITYLES = {
|
|
103
|
+
'No Data': 4,
|
|
104
|
+
'Water': 1,
|
|
105
|
+
'Trees': 5,
|
|
106
|
+
'Grass': 4,
|
|
107
|
+
'Flooded Vegetation': 2,
|
|
108
|
+
'Crops': 3,
|
|
109
|
+
'Scrub/Shrub': 4,
|
|
110
|
+
'Built Area': 10,
|
|
111
|
+
'Bare Ground': 9,
|
|
112
|
+
'Snow/Ice': 9,
|
|
113
|
+
'Clouds': 4
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Dynamic World V1
|
|
117
|
+
DYNAMIC_WORLD_CLASS_TO_CITYLES = {
|
|
118
|
+
'Water': 1,
|
|
119
|
+
'Trees': 5,
|
|
120
|
+
'Grass': 4,
|
|
121
|
+
'Flooded Vegetation': 2,
|
|
122
|
+
'Crops': 3,
|
|
123
|
+
'Shrub and Scrub': 4,
|
|
124
|
+
'Built': 10,
|
|
125
|
+
'Bare': 9,
|
|
126
|
+
'Snow and Ice': 9
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Building material mapping based on corrected documentation
|
|
61
130
|
BUILDING_MATERIAL_MAPPING = {
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
131
|
+
'building': 110, # Building (general)
|
|
132
|
+
'concrete': 110, # Building (concrete)
|
|
133
|
+
'residential': 111, # Old wooden house
|
|
134
|
+
'wooden': 111, # Old wooden house
|
|
135
|
+
'commercial': 110, # Building (commercial)
|
|
136
|
+
'industrial': 110, # Building (industrial)
|
|
137
|
+
'default': 110 # Default to general building
|
|
67
138
|
}
|
|
68
139
|
|
|
69
|
-
# Tree type mapping
|
|
140
|
+
# Tree type mapping for vmap.txt
|
|
70
141
|
TREE_TYPE_MAPPING = {
|
|
71
|
-
'deciduous': 101,
|
|
72
|
-
'evergreen': 101,
|
|
73
|
-
'
|
|
142
|
+
'deciduous': 101, # Leaf
|
|
143
|
+
'evergreen': 101, # Leaf (simplified)
|
|
144
|
+
'leaf': 101, # Leaf
|
|
145
|
+
'shade': 102, # Shade
|
|
146
|
+
'default': 101 # Default to leaf
|
|
74
147
|
}
|
|
75
148
|
|
|
76
149
|
|
|
@@ -81,44 +154,39 @@ def create_cityles_directories(output_directory):
|
|
|
81
154
|
return output_path
|
|
82
155
|
|
|
83
156
|
|
|
84
|
-
def
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
land_cover_source
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
return esri_mapping.get(land_cover_value, LANDCOVER_TO_CITYLES_LANDUSE['default'])
|
|
119
|
-
|
|
120
|
-
# Default mapping
|
|
121
|
-
return LANDCOVER_TO_CITYLES_LANDUSE['default']
|
|
157
|
+
def _get_source_name_mapping(land_cover_source):
|
|
158
|
+
"""Return the class-name-to-CityLES mapping dictionary for the given source."""
|
|
159
|
+
if land_cover_source == 'OpenStreetMap' or land_cover_source == 'Standard':
|
|
160
|
+
return OSM_CLASS_TO_CITYLES
|
|
161
|
+
if land_cover_source == 'Urbanwatch':
|
|
162
|
+
return URBANWATCH_CLASS_TO_CITYLES
|
|
163
|
+
if land_cover_source == 'OpenEarthMapJapan':
|
|
164
|
+
return OEMJ_CLASS_TO_CITYLES
|
|
165
|
+
if land_cover_source == 'ESA WorldCover':
|
|
166
|
+
return ESA_CLASS_TO_CITYLES
|
|
167
|
+
if land_cover_source == 'ESRI 10m Annual Land Cover':
|
|
168
|
+
return ESRI_CLASS_TO_CITYLES
|
|
169
|
+
if land_cover_source == 'Dynamic World V1':
|
|
170
|
+
return DYNAMIC_WORLD_CLASS_TO_CITYLES
|
|
171
|
+
# Default fallback
|
|
172
|
+
return OSM_CLASS_TO_CITYLES
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_index_to_cityles_map(land_cover_source):
|
|
176
|
+
"""Build mapping: raw per-source index -> CityLES code, using source class order."""
|
|
177
|
+
try:
|
|
178
|
+
from voxcity.utils.lc import get_land_cover_classes
|
|
179
|
+
class_dict = get_land_cover_classes(land_cover_source)
|
|
180
|
+
class_names = list(class_dict.values())
|
|
181
|
+
except Exception:
|
|
182
|
+
# Fallback: no class list; return empty so default is used
|
|
183
|
+
class_names = []
|
|
184
|
+
|
|
185
|
+
name_to_code = _get_source_name_mapping(land_cover_source)
|
|
186
|
+
index_to_code = {}
|
|
187
|
+
for idx, class_name in enumerate(class_names):
|
|
188
|
+
index_to_code[idx] = name_to_code.get(class_name, 4)
|
|
189
|
+
return index_to_code, class_names
|
|
122
190
|
|
|
123
191
|
|
|
124
192
|
def export_topog(building_height_grid, building_id_grid, output_path,
|
|
@@ -139,26 +207,26 @@ def export_topog(building_height_grid, building_id_grid, output_path,
|
|
|
139
207
|
"""
|
|
140
208
|
filename = output_path / 'topog.txt'
|
|
141
209
|
|
|
142
|
-
|
|
143
|
-
building_positions = np.argwhere(building_height_grid > 0)
|
|
144
|
-
n_buildings = len(building_positions)
|
|
145
|
-
|
|
210
|
+
ny, nx = building_height_grid.shape
|
|
146
211
|
material_code = BUILDING_MATERIAL_MAPPING.get(building_material,
|
|
147
212
|
BUILDING_MATERIAL_MAPPING['default'])
|
|
148
213
|
|
|
214
|
+
# Write all grid cells including those without buildings
|
|
215
|
+
n_buildings = ny * nx
|
|
216
|
+
|
|
149
217
|
with open(filename, 'w') as f:
|
|
150
218
|
# Write number of buildings
|
|
151
219
|
f.write(f"{n_buildings}\n")
|
|
152
220
|
|
|
153
|
-
# Write
|
|
154
|
-
for
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
221
|
+
# Write data for ALL grid points (buildings and non-buildings)
|
|
222
|
+
for j in range(ny):
|
|
223
|
+
for i in range(nx):
|
|
224
|
+
# CityLES uses 1-based indexing
|
|
225
|
+
i_1based = i + 1
|
|
226
|
+
j_1based = j + 1
|
|
227
|
+
height = float(building_height_grid[j, i])
|
|
228
|
+
# Format: i j height material_code depth1 depth2 changed_material
|
|
229
|
+
f.write(f"{i_1based} {j_1based} {height:.1f} {material_code} 0.0 0.0 102\n")
|
|
162
230
|
|
|
163
231
|
|
|
164
232
|
def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
@@ -168,7 +236,7 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
|
168
236
|
Parameters:
|
|
169
237
|
-----------
|
|
170
238
|
land_cover_grid : numpy.ndarray
|
|
171
|
-
2D array of land cover values
|
|
239
|
+
2D array of land cover values (may be raw or converted)
|
|
172
240
|
output_path : Path
|
|
173
241
|
Output directory path
|
|
174
242
|
land_cover_source : str, optional
|
|
@@ -176,13 +244,38 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
|
176
244
|
"""
|
|
177
245
|
filename = output_path / 'landuse.txt'
|
|
178
246
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
247
|
+
ny, nx = land_cover_grid.shape
|
|
248
|
+
|
|
249
|
+
# Build per-source index mapping
|
|
250
|
+
index_to_code, class_names = _build_index_to_cityles_map(land_cover_source)
|
|
251
|
+
|
|
252
|
+
print(f"Land cover source: {land_cover_source} (raw indices)")
|
|
253
|
+
|
|
254
|
+
# Create mapping statistics
|
|
255
|
+
mapping_stats = {}
|
|
256
|
+
|
|
182
257
|
with open(filename, 'w') as f:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
258
|
+
# Write in row-major order (j varies first, then i)
|
|
259
|
+
for j in range(ny):
|
|
260
|
+
for i in range(nx):
|
|
261
|
+
idx = int(land_cover_grid[j, i])
|
|
262
|
+
cityles_code = index_to_code.get(idx, 4)
|
|
263
|
+
f.write(f"{cityles_code}\n")
|
|
264
|
+
|
|
265
|
+
# Track mapping statistics
|
|
266
|
+
if idx not in mapping_stats:
|
|
267
|
+
mapping_stats[idx] = {'cityles_code': cityles_code, 'count': 0}
|
|
268
|
+
mapping_stats[idx]['count'] += 1
|
|
269
|
+
|
|
270
|
+
# Print mapping summary
|
|
271
|
+
print("\nLand cover mapping summary (by source class):")
|
|
272
|
+
total = ny * nx
|
|
273
|
+
for idx in sorted(mapping_stats.keys()):
|
|
274
|
+
stats = mapping_stats[idx]
|
|
275
|
+
percentage = (stats['count'] / total) * 100
|
|
276
|
+
class_name = class_names[idx] if 0 <= idx < len(class_names) else 'Unknown'
|
|
277
|
+
print(f" {idx}: {class_name} -> CityLES {stats['cityles_code']}: "
|
|
278
|
+
f"{stats['count']} cells ({percentage:.1f}%)")
|
|
186
279
|
|
|
187
280
|
|
|
188
281
|
def export_dem(dem_grid, output_path):
|
|
@@ -228,27 +321,27 @@ def export_vmap(canopy_height_grid, output_path, tree_base_ratio=0.3, tree_type=
|
|
|
228
321
|
"""
|
|
229
322
|
filename = output_path / 'vmap.txt'
|
|
230
323
|
|
|
231
|
-
|
|
232
|
-
tree_positions = np.argwhere(canopy_height_grid > 0)
|
|
233
|
-
n_trees = len(tree_positions)
|
|
234
|
-
|
|
324
|
+
ny, nx = canopy_height_grid.shape
|
|
235
325
|
tree_code = TREE_TYPE_MAPPING.get(tree_type, TREE_TYPE_MAPPING['default'])
|
|
236
326
|
|
|
327
|
+
# Write all grid cells including those without vegetation
|
|
328
|
+
n_trees = ny * nx
|
|
329
|
+
|
|
237
330
|
with open(filename, 'w') as f:
|
|
238
331
|
# Write number of trees
|
|
239
332
|
f.write(f"{n_trees}\n")
|
|
240
333
|
|
|
241
|
-
# Write
|
|
242
|
-
for
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
334
|
+
# Write data for ALL grid points (vegetation and non-vegetation)
|
|
335
|
+
for j in range(ny):
|
|
336
|
+
for i in range(nx):
|
|
337
|
+
# CityLES uses 1-based indexing
|
|
338
|
+
i_1based = i + 1
|
|
339
|
+
j_1based = j + 1
|
|
340
|
+
total_height = float(canopy_height_grid[j, i])
|
|
341
|
+
lower_height = total_height * tree_base_ratio
|
|
342
|
+
upper_height = total_height
|
|
343
|
+
# Format: i j lower_height upper_height tree_type
|
|
344
|
+
f.write(f"{i_1based} {j_1based} {lower_height:.1f} {upper_height:.1f} {tree_code}\n")
|
|
252
345
|
|
|
253
346
|
|
|
254
347
|
def export_lonlat(rectangle_vertices, grid_shape, output_path):
|
|
@@ -287,6 +380,7 @@ def export_lonlat(rectangle_vertices, grid_shape, output_path):
|
|
|
287
380
|
lon = lon_vals[i]
|
|
288
381
|
lat = lat_vals[j]
|
|
289
382
|
|
|
383
|
+
# Note: Format is i j longitude latitude (not latitude longitude)
|
|
290
384
|
f.write(f"{i_1based} {j_1based} {lon:.7f} {lat:.8f}\n")
|
|
291
385
|
|
|
292
386
|
|
|
@@ -307,13 +401,13 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
307
401
|
canopy_height_grid : numpy.ndarray
|
|
308
402
|
2D array of canopy heights
|
|
309
403
|
land_cover_grid : numpy.ndarray
|
|
310
|
-
2D array of land cover values
|
|
404
|
+
2D array of land cover values (may be raw or VoxCity standard)
|
|
311
405
|
dem_grid : numpy.ndarray
|
|
312
406
|
2D array of elevation values
|
|
313
407
|
meshsize : float
|
|
314
408
|
Grid cell size in meters
|
|
315
409
|
land_cover_source : str
|
|
316
|
-
Source of land cover data (e.g., '
|
|
410
|
+
Source of land cover data (e.g., 'ESRI 10m Annual Land Cover', 'ESA WorldCover')
|
|
317
411
|
rectangle_vertices : list of tuples
|
|
318
412
|
List of (lon, lat) vertices defining the area
|
|
319
413
|
output_directory : str
|
|
@@ -335,21 +429,22 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
335
429
|
output_path = create_cityles_directories(output_directory)
|
|
336
430
|
|
|
337
431
|
print(f"Exporting CityLES files to: {output_path}")
|
|
432
|
+
print(f"Land cover source: {land_cover_source}")
|
|
338
433
|
|
|
339
434
|
# Export individual files
|
|
340
|
-
print("
|
|
435
|
+
print("\nExporting topog.txt...")
|
|
341
436
|
export_topog(building_height_grid, building_id_grid, output_path, building_material)
|
|
342
437
|
|
|
343
|
-
print("
|
|
438
|
+
print("\nExporting landuse.txt...")
|
|
344
439
|
export_landuse(land_cover_grid, output_path, land_cover_source)
|
|
345
440
|
|
|
346
|
-
print("
|
|
441
|
+
print("\nExporting dem.txt...")
|
|
347
442
|
export_dem(dem_grid, output_path)
|
|
348
443
|
|
|
349
|
-
print("
|
|
444
|
+
print("\nExporting vmap.txt...")
|
|
350
445
|
export_vmap(canopy_height_grid, output_path, tree_base_ratio, tree_type)
|
|
351
446
|
|
|
352
|
-
print("
|
|
447
|
+
print("\nExporting lonlat.txt...")
|
|
353
448
|
export_lonlat(rectangle_vertices, building_height_grid.shape, output_path)
|
|
354
449
|
|
|
355
450
|
# Create metadata file for reference
|
|
@@ -357,12 +452,52 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
357
452
|
with open(metadata_file, 'w') as f:
|
|
358
453
|
f.write("CityLES Export Metadata\n")
|
|
359
454
|
f.write("====================\n")
|
|
455
|
+
f.write(f"Export date: 2025/08/05\n")
|
|
360
456
|
f.write(f"Grid shape: {building_height_grid.shape}\n")
|
|
361
457
|
f.write(f"Mesh size: {meshsize} m\n")
|
|
362
458
|
f.write(f"Land cover source: {land_cover_source}\n")
|
|
363
459
|
f.write(f"Building material: {building_material}\n")
|
|
364
460
|
f.write(f"Tree type: {tree_type}\n")
|
|
365
461
|
f.write(f"Bounds: {rectangle_vertices}\n")
|
|
462
|
+
f.write(f"Buildings: {np.sum(building_height_grid > 0)}\n")
|
|
463
|
+
f.write(f"Trees: {np.sum(canopy_height_grid > 0)}\n")
|
|
464
|
+
|
|
465
|
+
# Add land use value ranges
|
|
466
|
+
f.write(f"\nLand cover value range: {land_cover_grid.min()} - {land_cover_grid.max()}\n")
|
|
467
|
+
unique_values = np.unique(land_cover_grid)
|
|
468
|
+
f.write(f"Unique land cover values: {unique_values}\n")
|
|
366
469
|
|
|
367
|
-
print(f"
|
|
368
|
-
return str(output_path)
|
|
470
|
+
print(f"\nCityLES export completed successfully!")
|
|
471
|
+
return str(output_path)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Helper function to apply VoxCity's convert_land_cover if needed
|
|
475
|
+
def ensure_converted_land_cover(land_cover_grid, land_cover_source):
|
|
476
|
+
"""
|
|
477
|
+
Ensure land cover grid uses VoxCity standard indices
|
|
478
|
+
|
|
479
|
+
This function checks if the land cover data needs conversion and applies
|
|
480
|
+
VoxCity's convert_land_cover function if necessary.
|
|
481
|
+
|
|
482
|
+
Parameters:
|
|
483
|
+
-----------
|
|
484
|
+
land_cover_grid : numpy.ndarray
|
|
485
|
+
2D array of land cover values
|
|
486
|
+
land_cover_source : str
|
|
487
|
+
Source of land cover data
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
--------
|
|
491
|
+
numpy.ndarray : Land cover grid with VoxCity standard indices (0-13)
|
|
492
|
+
"""
|
|
493
|
+
# Import VoxCity's convert function if available
|
|
494
|
+
try:
|
|
495
|
+
from voxcity.utils.lc import convert_land_cover
|
|
496
|
+
|
|
497
|
+
# Apply conversion
|
|
498
|
+
converted_grid = convert_land_cover(land_cover_grid, land_cover_source)
|
|
499
|
+
print(f"Applied VoxCity land cover conversion for {land_cover_source}")
|
|
500
|
+
return converted_grid
|
|
501
|
+
except ImportError:
|
|
502
|
+
print("Warning: Could not import VoxCity land cover utilities. Using direct mapping.")
|
|
503
|
+
return land_cover_grid
|