voxcity 0.6.26__py3-none-any.whl → 0.7.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.
- voxcity/__init__.py +14 -8
- 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 +13 -5
- voxcity/exporter/cityles.py +633 -538
- voxcity/exporter/envimet.py +728 -708
- voxcity/exporter/magicavoxel.py +334 -297
- voxcity/exporter/netcdf.py +238 -211
- voxcity/exporter/obj.py +1481 -1406
- voxcity/generator/__init__.py +44 -0
- voxcity/generator/api.py +675 -0
- voxcity/generator/grids.py +379 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/voxelizer.py +380 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +62 -12
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +806 -790
- voxcity/geoprocessor/network.py +708 -679
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +428 -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 +156 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +18 -14
- 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 +43 -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/temporal.py +434 -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/logging.py +61 -0
- voxcity/utils/orientation.py +51 -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 +928 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/METADATA +107 -34
- voxcity-0.7.0.dist-info/RECORD +77 -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-0.7.0.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/LICENSE +0 -0
voxcity/exporter/cityles.py
CHANGED
|
@@ -1,539 +1,634 @@
|
|
|
1
|
-
"""
|
|
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'.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import os
|
|
15
|
-
import numpy as np
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
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
|
-
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
output_path
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
land_cover_source
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1
|
+
"""
|
|
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'.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import numpy as np
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from ..models import VoxCity
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# VoxCity standard land cover classes after conversion
|
|
21
|
+
# Based on convert_land_cover function output
|
|
22
|
+
VOXCITY_STANDARD_CLASSES = {
|
|
23
|
+
0: 'Bareland',
|
|
24
|
+
1: 'Rangeland',
|
|
25
|
+
2: 'Shrub',
|
|
26
|
+
3: 'Agriculture land',
|
|
27
|
+
4: 'Tree',
|
|
28
|
+
5: 'Moss and lichen',
|
|
29
|
+
6: 'Wet land',
|
|
30
|
+
7: 'Mangrove',
|
|
31
|
+
8: 'Water',
|
|
32
|
+
9: 'Snow and ice',
|
|
33
|
+
10: 'Developed space',
|
|
34
|
+
11: 'Road',
|
|
35
|
+
12: 'Building',
|
|
36
|
+
13: 'No Data'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
## Source-specific class name to CityLES land use mappings
|
|
40
|
+
# CityLES land use codes: 1=Water, 2=Rice Paddy, 3=Crops, 4=Grassland, 5=Deciduous Broadleaf Forest,
|
|
41
|
+
# 9=Bare Land, 10=Building, 16=Asphalt (road), etc.
|
|
42
|
+
|
|
43
|
+
# OpenStreetMap / Standard
|
|
44
|
+
OSM_CLASS_TO_CITYLES = {
|
|
45
|
+
'Bareland': 9,
|
|
46
|
+
'Rangeland': 4,
|
|
47
|
+
'Shrub': 4,
|
|
48
|
+
'Moss and lichen': 4,
|
|
49
|
+
'Agriculture land': 3,
|
|
50
|
+
'Tree': 5,
|
|
51
|
+
'Wet land': 2,
|
|
52
|
+
'Mangroves': 5,
|
|
53
|
+
'Water': 1,
|
|
54
|
+
'Snow and ice': 9,
|
|
55
|
+
'Developed space': 10,
|
|
56
|
+
'Road': 16,
|
|
57
|
+
'Building': 10,
|
|
58
|
+
'No Data': 4
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Urbanwatch
|
|
62
|
+
URBANWATCH_CLASS_TO_CITYLES = {
|
|
63
|
+
'Building': 10,
|
|
64
|
+
'Road': 16,
|
|
65
|
+
'Parking Lot': 16,
|
|
66
|
+
'Tree Canopy': 5,
|
|
67
|
+
'Grass/Shrub': 4,
|
|
68
|
+
'Agriculture': 3,
|
|
69
|
+
'Water': 1,
|
|
70
|
+
'Barren': 9,
|
|
71
|
+
'Unknown': 4,
|
|
72
|
+
'Sea': 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# OpenEarthMapJapan
|
|
76
|
+
OEMJ_CLASS_TO_CITYLES = {
|
|
77
|
+
'Bareland': 9,
|
|
78
|
+
'Rangeland': 4,
|
|
79
|
+
'Developed space': 10,
|
|
80
|
+
'Road': 16,
|
|
81
|
+
'Tree': 5,
|
|
82
|
+
'Water': 1,
|
|
83
|
+
'Agriculture land': 3,
|
|
84
|
+
'Building': 10
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ESA WorldCover
|
|
88
|
+
ESA_CLASS_TO_CITYLES = {
|
|
89
|
+
'Trees': 5,
|
|
90
|
+
'Shrubland': 4,
|
|
91
|
+
'Grassland': 4,
|
|
92
|
+
'Cropland': 3,
|
|
93
|
+
'Built-up': 10,
|
|
94
|
+
'Barren / sparse vegetation': 9,
|
|
95
|
+
'Snow and ice': 9,
|
|
96
|
+
'Open water': 1,
|
|
97
|
+
'Herbaceous wetland': 2,
|
|
98
|
+
'Mangroves': 5,
|
|
99
|
+
'Moss and lichen': 9
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ESRI 10m Annual Land Cover
|
|
103
|
+
ESRI_CLASS_TO_CITYLES = {
|
|
104
|
+
'No Data': 4,
|
|
105
|
+
'Water': 1,
|
|
106
|
+
'Trees': 5,
|
|
107
|
+
'Grass': 4,
|
|
108
|
+
'Flooded Vegetation': 2,
|
|
109
|
+
'Crops': 3,
|
|
110
|
+
'Scrub/Shrub': 4,
|
|
111
|
+
'Built Area': 10,
|
|
112
|
+
'Bare Ground': 9,
|
|
113
|
+
'Snow/Ice': 9,
|
|
114
|
+
'Clouds': 4
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Dynamic World V1
|
|
118
|
+
DYNAMIC_WORLD_CLASS_TO_CITYLES = {
|
|
119
|
+
'Water': 1,
|
|
120
|
+
'Trees': 5,
|
|
121
|
+
'Grass': 4,
|
|
122
|
+
'Flooded Vegetation': 2,
|
|
123
|
+
'Crops': 3,
|
|
124
|
+
'Shrub and Scrub': 4,
|
|
125
|
+
'Built': 10,
|
|
126
|
+
'Bare': 9,
|
|
127
|
+
'Snow and Ice': 9
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Building material mapping based on corrected documentation
|
|
131
|
+
BUILDING_MATERIAL_MAPPING = {
|
|
132
|
+
'building': 110, # Building (general)
|
|
133
|
+
'concrete': 110, # Building (concrete)
|
|
134
|
+
'residential': 111, # Old wooden house
|
|
135
|
+
'wooden': 111, # Old wooden house
|
|
136
|
+
'commercial': 110, # Building (commercial)
|
|
137
|
+
'industrial': 110, # Building (industrial)
|
|
138
|
+
'default': 110 # Default to general building
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Tree type mapping for vmap.txt
|
|
142
|
+
TREE_TYPE_MAPPING = {
|
|
143
|
+
'deciduous': 101, # Leaf
|
|
144
|
+
'evergreen': 101, # Leaf (simplified)
|
|
145
|
+
'leaf': 101, # Leaf
|
|
146
|
+
'shade': 102, # Shade
|
|
147
|
+
'default': 101 # Default to leaf
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def create_cityles_directories(output_directory):
|
|
152
|
+
"""Create necessary directories for CityLES output"""
|
|
153
|
+
output_path = Path(output_directory)
|
|
154
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
return output_path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_source_name_mapping(land_cover_source):
|
|
159
|
+
"""Return the class-name-to-CityLES mapping dictionary for the given source."""
|
|
160
|
+
if land_cover_source == 'OpenStreetMap' or land_cover_source == 'Standard':
|
|
161
|
+
return OSM_CLASS_TO_CITYLES
|
|
162
|
+
if land_cover_source == 'Urbanwatch':
|
|
163
|
+
return URBANWATCH_CLASS_TO_CITYLES
|
|
164
|
+
if land_cover_source == 'OpenEarthMapJapan':
|
|
165
|
+
return OEMJ_CLASS_TO_CITYLES
|
|
166
|
+
if land_cover_source == 'ESA WorldCover':
|
|
167
|
+
return ESA_CLASS_TO_CITYLES
|
|
168
|
+
if land_cover_source == 'ESRI 10m Annual Land Cover':
|
|
169
|
+
return ESRI_CLASS_TO_CITYLES
|
|
170
|
+
if land_cover_source == 'Dynamic World V1':
|
|
171
|
+
return DYNAMIC_WORLD_CLASS_TO_CITYLES
|
|
172
|
+
# Default fallback
|
|
173
|
+
return OSM_CLASS_TO_CITYLES
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _build_index_to_cityles_map(land_cover_source):
|
|
177
|
+
"""Build mapping: raw per-source index -> CityLES code, using source class order."""
|
|
178
|
+
try:
|
|
179
|
+
from voxcity.utils.lc import get_land_cover_classes
|
|
180
|
+
class_dict = get_land_cover_classes(land_cover_source)
|
|
181
|
+
class_names = list(class_dict.values())
|
|
182
|
+
except Exception:
|
|
183
|
+
# Fallback: no class list; return empty so default is used
|
|
184
|
+
class_names = []
|
|
185
|
+
|
|
186
|
+
name_to_code = _get_source_name_mapping(land_cover_source)
|
|
187
|
+
index_to_code = {}
|
|
188
|
+
for idx, class_name in enumerate(class_names):
|
|
189
|
+
index_to_code[idx] = name_to_code.get(class_name, 4)
|
|
190
|
+
return index_to_code, class_names
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _resolve_under_tree_code(under_tree_class_name, under_tree_cityles_code, land_cover_source):
|
|
194
|
+
"""Resolve the CityLES land-use code used under tree canopy.
|
|
195
|
+
|
|
196
|
+
Priority:
|
|
197
|
+
1) Explicit numeric code if provided
|
|
198
|
+
2) Class name using the source-specific mapping
|
|
199
|
+
3) Class name using the standard (OSM) mapping
|
|
200
|
+
4) Default to 9 (Bare Land)
|
|
201
|
+
"""
|
|
202
|
+
if under_tree_cityles_code is not None:
|
|
203
|
+
try:
|
|
204
|
+
return int(under_tree_cityles_code)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
name_to_code = _get_source_name_mapping(land_cover_source)
|
|
208
|
+
code = name_to_code.get(under_tree_class_name)
|
|
209
|
+
if code is None:
|
|
210
|
+
code = OSM_CLASS_TO_CITYLES.get(under_tree_class_name, 9)
|
|
211
|
+
return code
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def export_topog(building_height_grid, building_id_grid, output_path,
|
|
215
|
+
building_material='default', cityles_landuse_grid=None):
|
|
216
|
+
"""
|
|
217
|
+
Export topog.txt file for CityLES
|
|
218
|
+
|
|
219
|
+
Parameters:
|
|
220
|
+
-----------
|
|
221
|
+
building_height_grid : numpy.ndarray
|
|
222
|
+
2D array of building heights
|
|
223
|
+
building_id_grid : numpy.ndarray
|
|
224
|
+
2D array of building IDs
|
|
225
|
+
output_path : Path
|
|
226
|
+
Output directory path
|
|
227
|
+
building_material : str
|
|
228
|
+
Building material type for mapping
|
|
229
|
+
"""
|
|
230
|
+
filename = output_path / 'topog.txt'
|
|
231
|
+
|
|
232
|
+
ny, nx = building_height_grid.shape
|
|
233
|
+
material_code = BUILDING_MATERIAL_MAPPING.get(building_material,
|
|
234
|
+
BUILDING_MATERIAL_MAPPING['default'])
|
|
235
|
+
|
|
236
|
+
# Count only cells with building height > 0
|
|
237
|
+
building_mask = building_height_grid > 0
|
|
238
|
+
n_buildings = int(np.count_nonzero(building_mask))
|
|
239
|
+
|
|
240
|
+
with open(filename, 'w') as f:
|
|
241
|
+
# Write number of buildings
|
|
242
|
+
f.write(f"{n_buildings}\n")
|
|
243
|
+
|
|
244
|
+
# Write data for ALL grid points (buildings and non-buildings)
|
|
245
|
+
for j in range(ny):
|
|
246
|
+
for i in range(nx):
|
|
247
|
+
# CityLES uses 1-based indexing
|
|
248
|
+
i_1based = i + 1
|
|
249
|
+
j_1based = j + 1
|
|
250
|
+
height = float(building_height_grid[j, i])
|
|
251
|
+
# Decide material code per cell
|
|
252
|
+
if cityles_landuse_grid is not None:
|
|
253
|
+
cell_lu = int(cityles_landuse_grid[j, i])
|
|
254
|
+
material_code_cell = cell_lu + 100
|
|
255
|
+
else:
|
|
256
|
+
if height > 0:
|
|
257
|
+
material_code_cell = material_code
|
|
258
|
+
else:
|
|
259
|
+
material_code_cell = 102
|
|
260
|
+
# Format: i j height material_code depth1 depth2 changed_material
|
|
261
|
+
f.write(f"{i_1based} {j_1based} {height:.1f} {material_code_cell} 0.0 0.0 102\n")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def export_landuse(land_cover_grid, output_path, land_cover_source=None,
|
|
265
|
+
canopy_height_grid=None, building_height_grid=None,
|
|
266
|
+
under_tree_class_name='Bareland', under_tree_cityles_code=None):
|
|
267
|
+
"""
|
|
268
|
+
Export landuse.txt file for CityLES
|
|
269
|
+
|
|
270
|
+
Parameters:
|
|
271
|
+
-----------
|
|
272
|
+
land_cover_grid : numpy.ndarray
|
|
273
|
+
2D array of land cover values (may be raw or converted)
|
|
274
|
+
output_path : Path
|
|
275
|
+
Output directory path
|
|
276
|
+
land_cover_source : str, optional
|
|
277
|
+
Source of land cover data
|
|
278
|
+
canopy_height_grid : numpy.ndarray, optional
|
|
279
|
+
2D array of canopy heights; if provided, cells with canopy (>0) will be
|
|
280
|
+
assigned the ground class under the canopy instead of a tree class.
|
|
281
|
+
building_height_grid : numpy.ndarray, optional
|
|
282
|
+
2D array of building heights; if provided, canopy overrides will not be
|
|
283
|
+
applied where buildings exist (height > 0).
|
|
284
|
+
under_tree_class_name : str, optional
|
|
285
|
+
Name of ground land-cover class to use under tree canopy. Defaults to 'Bareland'.
|
|
286
|
+
under_tree_cityles_code : int, optional
|
|
287
|
+
Explicit CityLES land-use code to use under canopy; if provided it takes
|
|
288
|
+
precedence over under_tree_class_name.
|
|
289
|
+
"""
|
|
290
|
+
filename = output_path / 'landuse.txt'
|
|
291
|
+
|
|
292
|
+
ny, nx = land_cover_grid.shape
|
|
293
|
+
|
|
294
|
+
# Build per-source index mapping
|
|
295
|
+
index_to_code, class_names = _build_index_to_cityles_map(land_cover_source)
|
|
296
|
+
|
|
297
|
+
print(f"Land cover source: {land_cover_source} (raw indices)")
|
|
298
|
+
|
|
299
|
+
# Resolve the CityLES code to use under tree canopy
|
|
300
|
+
under_tree_code = _resolve_under_tree_code(
|
|
301
|
+
under_tree_class_name, under_tree_cityles_code, land_cover_source
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Create mapping statistics: per raw index, count per resulting CityLES code
|
|
305
|
+
mapping_stats = {}
|
|
306
|
+
# Prepare grid to return
|
|
307
|
+
cityles_landuse_grid = np.zeros((ny, nx), dtype=int)
|
|
308
|
+
|
|
309
|
+
with open(filename, 'w') as f:
|
|
310
|
+
# Write in row-major order (j varies first, then i)
|
|
311
|
+
for j in range(ny):
|
|
312
|
+
for i in range(nx):
|
|
313
|
+
idx = int(land_cover_grid[j, i])
|
|
314
|
+
cityles_code = index_to_code.get(idx, 4)
|
|
315
|
+
|
|
316
|
+
# If a canopy grid is provided, override tree canopy cells to the
|
|
317
|
+
# specified ground class, optionally skipping where buildings exist.
|
|
318
|
+
if canopy_height_grid is not None:
|
|
319
|
+
has_canopy = float(canopy_height_grid[j, i]) > 0.0
|
|
320
|
+
has_building = False
|
|
321
|
+
if building_height_grid is not None:
|
|
322
|
+
has_building = float(building_height_grid[j, i]) > 0.0
|
|
323
|
+
if has_canopy and not has_building:
|
|
324
|
+
cityles_code = under_tree_code
|
|
325
|
+
f.write(f"{cityles_code}\n")
|
|
326
|
+
|
|
327
|
+
cityles_landuse_grid[j, i] = cityles_code
|
|
328
|
+
|
|
329
|
+
# Track mapping statistics
|
|
330
|
+
if idx not in mapping_stats:
|
|
331
|
+
mapping_stats[idx] = {}
|
|
332
|
+
mapping_stats[idx][cityles_code] = mapping_stats[idx].get(cityles_code, 0) + 1
|
|
333
|
+
|
|
334
|
+
# Print mapping summary
|
|
335
|
+
print("\nLand cover mapping summary (by source class):")
|
|
336
|
+
total = ny * nx
|
|
337
|
+
for idx in sorted(mapping_stats.keys()):
|
|
338
|
+
class_name = class_names[idx] if 0 <= idx < len(class_names) else 'Unknown'
|
|
339
|
+
for code, count in sorted(mapping_stats[idx].items()):
|
|
340
|
+
percentage = (count / total) * 100
|
|
341
|
+
print(f" {idx}: {class_name} -> CityLES {code}: {count} cells ({percentage:.1f}%)")
|
|
342
|
+
|
|
343
|
+
return cityles_landuse_grid
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def export_dem(dem_grid, output_path):
|
|
347
|
+
"""
|
|
348
|
+
Export dem.txt file for CityLES
|
|
349
|
+
|
|
350
|
+
Parameters:
|
|
351
|
+
-----------
|
|
352
|
+
dem_grid : numpy.ndarray
|
|
353
|
+
2D array of elevation values
|
|
354
|
+
output_path : Path
|
|
355
|
+
Output directory path
|
|
356
|
+
"""
|
|
357
|
+
filename = output_path / 'dem.txt'
|
|
358
|
+
|
|
359
|
+
ny, nx = dem_grid.shape
|
|
360
|
+
|
|
361
|
+
with open(filename, 'w') as f:
|
|
362
|
+
for j in range(ny):
|
|
363
|
+
for i in range(nx):
|
|
364
|
+
# CityLES uses 1-based indexing
|
|
365
|
+
i_1based = i + 1
|
|
366
|
+
j_1based = j + 1
|
|
367
|
+
elevation = float(dem_grid[j, i])
|
|
368
|
+
# Clamp negative elevations to 0.0 meters
|
|
369
|
+
if elevation < 0.0:
|
|
370
|
+
elevation = 0.0
|
|
371
|
+
f.write(f"{i_1based} {j_1based} {elevation:.1f}\n")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def export_vmap(canopy_height_grid, output_path, trunk_height_ratio=0.3, tree_type='default', building_height_grid=None, canopy_bottom_height_grid=None):
|
|
375
|
+
"""
|
|
376
|
+
Export vmap.txt file for CityLES
|
|
377
|
+
|
|
378
|
+
Parameters:
|
|
379
|
+
-----------
|
|
380
|
+
canopy_height_grid : numpy.ndarray
|
|
381
|
+
2D array of canopy heights
|
|
382
|
+
output_path : Path
|
|
383
|
+
Output directory path
|
|
384
|
+
trunk_height_ratio : float
|
|
385
|
+
Ratio of tree base height to total canopy height
|
|
386
|
+
tree_type : str
|
|
387
|
+
Tree type for mapping
|
|
388
|
+
"""
|
|
389
|
+
filename = output_path / 'vmap.txt'
|
|
390
|
+
|
|
391
|
+
ny, nx = canopy_height_grid.shape
|
|
392
|
+
tree_code = TREE_TYPE_MAPPING.get(tree_type, TREE_TYPE_MAPPING['default'])
|
|
393
|
+
|
|
394
|
+
# If building heights are provided, remove trees where buildings exist
|
|
395
|
+
if building_height_grid is not None:
|
|
396
|
+
effective_canopy = np.where(building_height_grid > 0, 0.0, canopy_height_grid)
|
|
397
|
+
else:
|
|
398
|
+
effective_canopy = canopy_height_grid
|
|
399
|
+
|
|
400
|
+
# Count only cells with canopy height > 0
|
|
401
|
+
vegetation_mask = effective_canopy > 0
|
|
402
|
+
n_trees = int(np.count_nonzero(vegetation_mask))
|
|
403
|
+
|
|
404
|
+
with open(filename, 'w') as f:
|
|
405
|
+
# Write number of trees
|
|
406
|
+
f.write(f"{n_trees}\n")
|
|
407
|
+
|
|
408
|
+
# Write data for ALL grid points (vegetation and non-vegetation)
|
|
409
|
+
for j in range(ny):
|
|
410
|
+
for i in range(nx):
|
|
411
|
+
# CityLES uses 1-based indexing
|
|
412
|
+
i_1based = i + 1
|
|
413
|
+
j_1based = j + 1
|
|
414
|
+
total_height = float(effective_canopy[j, i])
|
|
415
|
+
if canopy_bottom_height_grid is not None:
|
|
416
|
+
lower_height = float(np.clip(canopy_bottom_height_grid[j, i], 0.0, total_height))
|
|
417
|
+
else:
|
|
418
|
+
lower_height = total_height * trunk_height_ratio
|
|
419
|
+
upper_height = total_height
|
|
420
|
+
# Format: i j lower_height upper_height tree_type
|
|
421
|
+
f.write(f"{i_1based} {j_1based} {lower_height:.1f} {upper_height:.1f} {tree_code}\n")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def export_lonlat(rectangle_vertices, grid_shape, output_path):
|
|
425
|
+
"""
|
|
426
|
+
Export lonlat.txt file for CityLES
|
|
427
|
+
|
|
428
|
+
Parameters:
|
|
429
|
+
-----------
|
|
430
|
+
rectangle_vertices : list of tuples
|
|
431
|
+
List of (lon, lat) vertices defining the area
|
|
432
|
+
grid_shape : tuple
|
|
433
|
+
Shape of the grid (ny, nx)
|
|
434
|
+
output_path : Path
|
|
435
|
+
Output directory path
|
|
436
|
+
"""
|
|
437
|
+
filename = output_path / 'lonlat.txt'
|
|
438
|
+
|
|
439
|
+
ny, nx = grid_shape
|
|
440
|
+
|
|
441
|
+
# Extract bounds from vertices
|
|
442
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
443
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
444
|
+
min_lon, max_lon = min(lons), max(lons)
|
|
445
|
+
min_lat, max_lat = min(lats), max(lats)
|
|
446
|
+
|
|
447
|
+
# Create coordinate grids
|
|
448
|
+
lon_vals = np.linspace(min_lon, max_lon, nx)
|
|
449
|
+
lat_vals = np.linspace(min_lat, max_lat, ny)
|
|
450
|
+
|
|
451
|
+
with open(filename, 'w') as f:
|
|
452
|
+
for j in range(ny):
|
|
453
|
+
for i in range(nx):
|
|
454
|
+
# CityLES uses 1-based indexing
|
|
455
|
+
i_1based = i + 1
|
|
456
|
+
j_1based = j + 1
|
|
457
|
+
lon = lon_vals[i]
|
|
458
|
+
lat = lat_vals[j]
|
|
459
|
+
|
|
460
|
+
# Note: Format is i j longitude latitude (not latitude longitude)
|
|
461
|
+
f.write(f"{i_1based} {j_1based} {lon:.7f} {lat:.8f}\n")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def export_cityles(city: VoxCity,
|
|
465
|
+
output_directory: str = "output/cityles",
|
|
466
|
+
building_material: str = 'default',
|
|
467
|
+
tree_type: str = 'default',
|
|
468
|
+
trunk_height_ratio: float = 0.3,
|
|
469
|
+
canopy_bottom_height_grid=None,
|
|
470
|
+
under_tree_class_name: str = 'Bareland',
|
|
471
|
+
under_tree_cityles_code=None,
|
|
472
|
+
land_cover_source: str | None = None,
|
|
473
|
+
**kwargs):
|
|
474
|
+
"""
|
|
475
|
+
Export VoxCity data to CityLES format
|
|
476
|
+
|
|
477
|
+
Parameters:
|
|
478
|
+
-----------
|
|
479
|
+
building_height_grid : numpy.ndarray
|
|
480
|
+
2D array of building heights
|
|
481
|
+
building_id_grid : numpy.ndarray
|
|
482
|
+
2D array of building IDs
|
|
483
|
+
canopy_height_grid : numpy.ndarray
|
|
484
|
+
2D array of canopy heights
|
|
485
|
+
land_cover_grid : numpy.ndarray
|
|
486
|
+
2D array of land cover values (may be raw or VoxCity standard)
|
|
487
|
+
dem_grid : numpy.ndarray
|
|
488
|
+
2D array of elevation values
|
|
489
|
+
meshsize : float
|
|
490
|
+
Grid cell size in meters
|
|
491
|
+
land_cover_source : str
|
|
492
|
+
Source of land cover data (e.g., 'ESRI 10m Annual Land Cover', 'ESA WorldCover')
|
|
493
|
+
rectangle_vertices : list of tuples
|
|
494
|
+
List of (lon, lat) vertices defining the area
|
|
495
|
+
output_directory : str
|
|
496
|
+
Output directory path
|
|
497
|
+
building_material : str
|
|
498
|
+
Building material type for mapping
|
|
499
|
+
tree_type : str
|
|
500
|
+
Tree type for mapping
|
|
501
|
+
trunk_height_ratio : float
|
|
502
|
+
Ratio of tree base height to total canopy height
|
|
503
|
+
**kwargs : dict
|
|
504
|
+
Additional parameters (for compatibility)
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
--------
|
|
508
|
+
str : Path to output directory
|
|
509
|
+
"""
|
|
510
|
+
# Create output directory
|
|
511
|
+
output_path = create_cityles_directories(output_directory)
|
|
512
|
+
|
|
513
|
+
print(f"Exporting CityLES files to: {output_path}")
|
|
514
|
+
# Resolve data from VoxCity
|
|
515
|
+
building_height_grid = city.buildings.heights
|
|
516
|
+
building_id_grid = city.buildings.ids if city.buildings.ids is not None else np.zeros_like(building_height_grid, dtype=int)
|
|
517
|
+
canopy_height_grid = city.tree_canopy.top if city.tree_canopy is not None else np.zeros_like(city.land_cover.classes, dtype=float)
|
|
518
|
+
land_cover_grid = city.land_cover.classes
|
|
519
|
+
dem_grid = city.dem.elevation
|
|
520
|
+
meshsize = float(city.voxels.meta.meshsize)
|
|
521
|
+
rectangle_vertices = city.extras.get("rectangle_vertices") or [(0.0, 0.0)] * 4
|
|
522
|
+
land_cover_source = land_cover_source or city.extras.get("land_cover_source", "Standard")
|
|
523
|
+
|
|
524
|
+
print(f"Land cover source: {land_cover_source}")
|
|
525
|
+
|
|
526
|
+
# Export individual files
|
|
527
|
+
print("\nExporting landuse.txt...")
|
|
528
|
+
cityles_landuse_grid = export_landuse(
|
|
529
|
+
land_cover_grid,
|
|
530
|
+
output_path,
|
|
531
|
+
land_cover_source,
|
|
532
|
+
canopy_height_grid=canopy_height_grid,
|
|
533
|
+
building_height_grid=building_height_grid,
|
|
534
|
+
under_tree_class_name=under_tree_class_name,
|
|
535
|
+
under_tree_cityles_code=under_tree_cityles_code,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
print("\nExporting topog.txt...")
|
|
539
|
+
export_topog(
|
|
540
|
+
building_height_grid,
|
|
541
|
+
building_id_grid,
|
|
542
|
+
output_path,
|
|
543
|
+
building_material,
|
|
544
|
+
cityles_landuse_grid=cityles_landuse_grid,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
print("\nExporting dem.txt...")
|
|
548
|
+
export_dem(dem_grid, output_path)
|
|
549
|
+
|
|
550
|
+
print("\nExporting vmap.txt...")
|
|
551
|
+
export_vmap(canopy_height_grid, output_path, trunk_height_ratio, tree_type, building_height_grid=building_height_grid, canopy_bottom_height_grid=canopy_bottom_height_grid)
|
|
552
|
+
|
|
553
|
+
print("\nExporting lonlat.txt...")
|
|
554
|
+
export_lonlat(rectangle_vertices, building_height_grid.shape, output_path)
|
|
555
|
+
|
|
556
|
+
# Create metadata file for reference
|
|
557
|
+
metadata_file = output_path / 'cityles_metadata.txt'
|
|
558
|
+
with open(metadata_file, 'w') as f:
|
|
559
|
+
f.write("CityLES Export Metadata\n")
|
|
560
|
+
f.write("====================\n")
|
|
561
|
+
f.write(f"Export date: 2025/08/05\n")
|
|
562
|
+
f.write(f"Grid shape: {building_height_grid.shape}\n")
|
|
563
|
+
f.write(f"Mesh size: {meshsize} m\n")
|
|
564
|
+
f.write(f"Land cover source: {land_cover_source}\n")
|
|
565
|
+
f.write(f"Building material: {building_material}\n")
|
|
566
|
+
f.write(f"Tree type: {tree_type}\n")
|
|
567
|
+
f.write(f"Bounds: {rectangle_vertices}\n")
|
|
568
|
+
f.write(f"Buildings: {np.sum(building_height_grid > 0)}\n")
|
|
569
|
+
# Trees count after removing overlaps with buildings
|
|
570
|
+
trees_count = int(np.sum(np.where(building_height_grid > 0, 0.0, canopy_height_grid) > 0))
|
|
571
|
+
f.write(f"Trees: {trees_count}\n")
|
|
572
|
+
# Under-tree land-use selection
|
|
573
|
+
under_tree_code = _resolve_under_tree_code(
|
|
574
|
+
under_tree_class_name, under_tree_cityles_code, land_cover_source
|
|
575
|
+
)
|
|
576
|
+
f.write(f"Under-tree land use: {under_tree_class_name} (CityLES {under_tree_code})\n")
|
|
577
|
+
|
|
578
|
+
# Add land use value ranges
|
|
579
|
+
f.write(f"\nLand cover value range: {land_cover_grid.min()} - {land_cover_grid.max()}\n")
|
|
580
|
+
unique_values = np.unique(land_cover_grid)
|
|
581
|
+
f.write(f"Unique land cover values: {unique_values}\n")
|
|
582
|
+
|
|
583
|
+
print(f"\nCityLES export completed successfully!")
|
|
584
|
+
return str(output_path)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class CityLesExporter:
|
|
588
|
+
"""Exporter adapter to write a VoxCity model to CityLES text files."""
|
|
589
|
+
|
|
590
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
591
|
+
if not isinstance(obj, VoxCity):
|
|
592
|
+
raise TypeError("CityLesExporter expects a VoxCity instance")
|
|
593
|
+
city: VoxCity = obj
|
|
594
|
+
# CityLES writes multiple files; use output_directory/base_filename as folder/name
|
|
595
|
+
out_dir = os.path.join(output_directory, base_filename)
|
|
596
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
597
|
+
export_cityles(
|
|
598
|
+
city,
|
|
599
|
+
output_directory=out_dir,
|
|
600
|
+
**kwargs,
|
|
601
|
+
)
|
|
602
|
+
return out_dir
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# Helper function to apply VoxCity's convert_land_cover if needed
|
|
606
|
+
def ensure_converted_land_cover(land_cover_grid, land_cover_source):
|
|
607
|
+
"""
|
|
608
|
+
Ensure land cover grid uses VoxCity standard indices
|
|
609
|
+
|
|
610
|
+
This function checks if the land cover data needs conversion and applies
|
|
611
|
+
VoxCity's convert_land_cover function if necessary.
|
|
612
|
+
|
|
613
|
+
Parameters:
|
|
614
|
+
-----------
|
|
615
|
+
land_cover_grid : numpy.ndarray
|
|
616
|
+
2D array of land cover values
|
|
617
|
+
land_cover_source : str
|
|
618
|
+
Source of land cover data
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
--------
|
|
622
|
+
numpy.ndarray : Land cover grid with VoxCity standard indices (0-13)
|
|
623
|
+
"""
|
|
624
|
+
# Import VoxCity's convert function if available
|
|
625
|
+
try:
|
|
626
|
+
from voxcity.utils.lc import convert_land_cover
|
|
627
|
+
|
|
628
|
+
# Apply conversion
|
|
629
|
+
converted_grid = convert_land_cover(land_cover_grid, land_cover_source)
|
|
630
|
+
print(f"Applied VoxCity land cover conversion for {land_cover_source}")
|
|
631
|
+
return converted_grid
|
|
632
|
+
except ImportError:
|
|
633
|
+
print("Warning: Could not import VoxCity land cover utilities. Using direct mapping.")
|
|
539
634
|
return land_cover_grid
|