voxcity 0.5.18__py3-none-any.whl → 0.5.20__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.

@@ -1,2579 +1,2579 @@
1
- """
2
- VoxelCity Visualization Utilities
3
-
4
- This module provides comprehensive visualization tools for 3D voxel city data,
5
- including support for multiple color schemes, 3D plotting with matplotlib and plotly,
6
- grid visualization on basemaps, and mesh-based rendering with PyVista.
7
-
8
- The module handles various data types including:
9
- - Land cover classifications
10
- - Building heights and footprints
11
- - Digital elevation models (DEM)
12
- - Canopy heights
13
- - View indices (sky view factor, green view index)
14
- - Simulation results on building surfaces
15
-
16
- Key Features:
17
- - Multiple predefined color schemes for voxel visualization
18
- - 2D and 3D plotting capabilities
19
- - Interactive web maps with folium
20
- - Mesh export functionality (OBJ format)
21
- - Multi-view scene generation
22
- - Custom simulation result overlays
23
- """
24
-
25
- import numpy as np
26
- import matplotlib.pyplot as plt
27
- from mpl_toolkits.mplot3d import Axes3D
28
- from tqdm import tqdm
29
- import matplotlib.colors as mcolors
30
- from matplotlib.colors import ListedColormap, BoundaryNorm
31
- import matplotlib.cm as cm
32
- import contextily as ctx
33
- from shapely.geometry import Polygon
34
- import plotly.graph_objects as go
35
- from tqdm import tqdm
36
- import pyproj
37
- # import rasterio
38
- from pyproj import CRS
39
- # from shapely.geometry import box
40
- import seaborn as sns
41
- import random
42
- import folium
43
- import math
44
- import trimesh
45
- import pyvista as pv
46
- from IPython.display import display
47
- import os
48
-
49
- # Import utility functions for land cover classification
50
- from .lc import get_land_cover_classes
51
- # from ..geo.geojson import filter_buildings
52
-
53
- # Import grid processing functions
54
- from ..geoprocessor.grid import (
55
- calculate_grid_size,
56
- create_coordinate_mesh,
57
- create_cell_polygon,
58
- grid_to_geodataframe
59
- )
60
-
61
- # Import geospatial utility functions
62
- from ..geoprocessor.utils import (
63
- initialize_geod,
64
- calculate_distance,
65
- normalize_to_one_meter,
66
- setup_transformer,
67
- transform_coords,
68
- )
69
-
70
- # Import mesh generation and export functions
71
- from ..geoprocessor.mesh import (
72
- create_voxel_mesh,
73
- create_sim_surface_mesh,
74
- create_city_meshes,
75
- export_meshes,
76
- save_obj_from_colored_mesh
77
- )
78
- # from ..exporter.obj import save_obj_from_colored_mesh
79
-
80
- # Import material property functions
81
- from .material import get_material_dict
82
-
83
- def get_voxel_color_map(color_scheme='default'):
84
- """
85
- Returns a color map for voxel visualization based on the specified color scheme.
86
-
87
- This function provides multiple predefined color schemes for visualizing voxel data.
88
- Each scheme maps voxel class IDs to RGB color values [0-255]. The class IDs follow
89
- a specific convention where negative values represent built environment elements
90
- and positive values represent natural/ground surface elements.
91
-
92
- Voxel Class ID Convention:
93
- -99: Void/empty space (black)
94
- -30: Landmark buildings (special highlighting)
95
- -17 to -11: Building materials (plaster, glass, stone, metal, concrete, wood, brick)
96
- -3: Generic building structures
97
- -2: Trees/vegetation (above ground)
98
- -1: Underground/subsurface
99
- 1-14: Ground surface land cover types (bareland, vegetation, water, etc.)
100
-
101
- Parameters:
102
- -----------
103
- color_scheme : str, optional
104
- The name of the color scheme to use. Available options:
105
-
106
- Basic Schemes:
107
- - 'default': Original balanced color scheme for general use
108
- - 'high_contrast': High contrast colors for better visibility
109
- - 'monochrome': Shades of blue for academic presentations
110
- - 'pastel': Softer, muted colors for aesthetic appeal
111
- - 'dark_mode': Darker colors for dark backgrounds
112
- - 'grayscale': Black and white gradient with color accents
113
-
114
- Thematic Schemes:
115
- - 'autumn': Warm reds, oranges, and browns
116
- - 'cool': Cool blues, purples, and cyans
117
- - 'earth_tones': Natural earth colors
118
- - 'vibrant': Very bright, saturated colors
119
-
120
- Stylistic Schemes:
121
- - 'cyberpunk': Neon-like purples, pinks, and blues
122
- - 'tropical': Vibrant greens, oranges, pinks (island vibes)
123
- - 'vintage': Muted, sepia-like tones
124
- - 'neon_dreams': Super-bright, nightclub neon palette
125
-
126
- Returns:
127
- --------
128
- dict
129
- A dictionary mapping voxel class IDs (int) to RGB color values (list of 3 ints [0-255])
130
-
131
- Examples:
132
- ---------
133
- >>> colors = get_voxel_color_map('default')
134
- >>> print(colors[-3]) # Building color
135
- [180, 187, 216]
136
-
137
- >>> colors = get_voxel_color_map('cyberpunk')
138
- >>> print(colors[9]) # Water color in cyberpunk scheme
139
- [51, 0, 102]
140
-
141
- Notes:
142
- ------
143
- - All color values are in RGB format with range [0, 255]
144
- - The 'default' scheme should not be modified to maintain consistency
145
- - Unknown color schemes will fall back to 'default' with a warning
146
- - Color schemes can be extended by adding new elif blocks
147
- """
148
- # ----------------------
149
- # DO NOT MODIFY DEFAULT
150
- # ----------------------
151
- if color_scheme == 'default':
152
- return {
153
- -99: [0, 0, 0], # void,
154
- -30: [255, 0, 102], # (Pink) 'Landmark',
155
- -17: [238, 242, 234], # (light gray) 'plaster',
156
- -16: [56, 78, 84], # (Dark blue) 'glass',
157
- -15: [147, 140, 114], # (Light brown) 'stone',
158
- -14: [139, 149, 159], # (Gray) 'metal',
159
- -13: [186, 187, 181], # (Gray) 'concrete',
160
- -12: [248, 166, 2], # (Orange) 'wood',
161
- -11: [81, 59, 56], # (Dark red) 'brick',
162
- -3: [180, 187, 216], # Building
163
- -2: [78, 99, 63], # Tree
164
- -1: [188, 143, 143], # Underground
165
- 1: [239, 228, 176], # 'Bareland (ground surface)',
166
- 2: [123, 130, 59], # 'Rangeland (ground surface)',
167
- 3: [97, 140, 86], # 'Shrub (ground surface)',
168
- 4: [112, 120, 56], # 'Agriculture land (ground surface)',
169
- 5: [116, 150, 66], # 'Tree (ground surface)',
170
- 6: [187, 204, 40], # 'Moss and lichen (ground surface)',
171
- 7: [77, 118, 99], # 'Wet land (ground surface)',
172
- 8: [22, 61, 51], # 'Mangrove (ground surface)',
173
- 9: [44, 66, 133], # 'Water (ground surface)',
174
- 10: [205, 215, 224], # 'Snow and ice (ground surface)',
175
- 11: [108, 119, 129], # 'Developed space (ground surface)',
176
- 12: [59, 62, 87], # 'Road (ground surface)',
177
- 13: [150, 166, 190], # 'Building (ground surface)'
178
- 14: [239, 228, 176], # 'No Data (ground surface)'
179
- }
180
-
181
- elif color_scheme == 'high_contrast':
182
- return {
183
- -99: [0, 0, 0], # void
184
- -30: [255, 0, 255], # (Bright Magenta) 'Landmark'
185
- -17: [255, 255, 255], # (Pure White) 'plaster'
186
- -16: [0, 0, 255], # (Bright Blue) 'glass'
187
- -15: [153, 76, 0], # (Dark Brown) 'stone'
188
- -14: [192, 192, 192], # (Silver) 'metal'
189
- -13: [128, 128, 128], # (Gray) 'concrete'
190
- -12: [255, 128, 0], # (Bright Orange) 'wood'
191
- -11: [153, 0, 0], # (Dark Red) 'brick'
192
- -3: [0, 255, 255], # (Cyan) Building
193
- -2: [0, 153, 0], # (Green) Tree
194
- -1: [204, 0, 102], # (Dark Pink) Underground
195
- 1: [255, 255, 153], # (Light Yellow) 'Bareland'
196
- 2: [102, 153, 0], # (Olive Green) 'Rangeland'
197
- 3: [0, 204, 0], # (Bright Green) 'Shrub'
198
- 4: [153, 204, 0], # (Yellowish Green) 'Agriculture land'
199
- 5: [0, 102, 0], # (Dark Green) 'Tree'
200
- 6: [204, 255, 51], # (Lime Green) 'Moss and lichen'
201
- 7: [0, 153, 153], # (Teal) 'Wet land'
202
- 8: [0, 51, 0], # (Very Dark Green) 'Mangrove'
203
- 9: [0, 102, 204], # (Bright Blue) 'Water'
204
- 10: [255, 255, 255], # (White) 'Snow and ice'
205
- 11: [76, 76, 76], # (Dark Gray) 'Developed space'
206
- 12: [0, 0, 0], # (Black) 'Road'
207
- 13: [102, 102, 255], # (Light Purple) 'Building'
208
- 14: [255, 204, 153], # (Light Orange) 'No Data'
209
- }
210
-
211
- elif color_scheme == 'monochrome':
212
- return {
213
- -99: [0, 0, 0], # void
214
- -30: [28, 28, 99], # 'Landmark'
215
- -17: [242, 242, 242], # 'plaster'
216
- -16: [51, 51, 153], # 'glass'
217
- -15: [102, 102, 204], # 'stone'
218
- -14: [153, 153, 204], # 'metal'
219
- -13: [204, 204, 230], # 'concrete'
220
- -12: [76, 76, 178], # 'wood'
221
- -11: [25, 25, 127], # 'brick'
222
- -3: [179, 179, 230], # Building
223
- -2: [51, 51, 153], # Tree
224
- -1: [102, 102, 178], # Underground
225
- 1: [230, 230, 255], # 'Bareland'
226
- 2: [128, 128, 204], # 'Rangeland'
227
- 3: [102, 102, 204], # 'Shrub'
228
- 4: [153, 153, 230], # 'Agriculture land'
229
- 5: [76, 76, 178], # 'Tree'
230
- 6: [204, 204, 255], # 'Moss and lichen'
231
- 7: [76, 76, 178], # 'Wet land'
232
- 8: [25, 25, 127], # 'Mangrove'
233
- 9: [51, 51, 204], # 'Water'
234
- 10: [242, 242, 255], # 'Snow and ice'
235
- 11: [128, 128, 178], # 'Developed space'
236
- 12: [51, 51, 127], # 'Road'
237
- 13: [153, 153, 204], # 'Building'
238
- 14: [230, 230, 255], # 'No Data'
239
- }
240
-
241
- elif color_scheme == 'pastel':
242
- return {
243
- -99: [0, 0, 0], # void
244
- -30: [255, 179, 217], # (Pastel Pink) 'Landmark'
245
- -17: [245, 245, 245], # (Off White) 'plaster'
246
- -16: [173, 196, 230], # (Pastel Blue) 'glass'
247
- -15: [222, 213, 196], # (Pastel Brown) 'stone'
248
- -14: [211, 219, 226], # (Pastel Gray) 'metal'
249
- -13: [226, 226, 226], # (Light Gray) 'concrete'
250
- -12: [255, 223, 179], # (Pastel Orange) 'wood'
251
- -11: [204, 168, 166], # (Pastel Red) 'brick'
252
- -3: [214, 217, 235], # (Pastel Purple) Building
253
- -2: [190, 207, 180], # (Pastel Green) Tree
254
- -1: [235, 204, 204], # (Pastel Pink) Underground
255
- 1: [250, 244, 227], # (Cream) 'Bareland'
256
- 2: [213, 217, 182], # (Pastel Olive) 'Rangeland'
257
- 3: [200, 226, 195], # (Pastel Green) 'Shrub'
258
- 4: [209, 214, 188], # (Pastel Yellow-Green) 'Agriculture land'
259
- 5: [195, 220, 168], # (Light Pastel Green) 'Tree'
260
- 6: [237, 241, 196], # (Pastel Yellow) 'Moss and lichen'
261
- 7: [180, 210, 205], # (Pastel Teal) 'Wet land'
262
- 8: [176, 196, 190], # (Darker Pastel Teal) 'Mangrove'
263
- 9: [188, 206, 235], # (Pastel Blue) 'Water'
264
- 10: [242, 245, 250], # (Light Blue-White) 'Snow and ice'
265
- 11: [209, 213, 219], # (Pastel Gray) 'Developed space'
266
- 12: [189, 190, 204], # (Pastel Blue-Gray) 'Road'
267
- 13: [215, 221, 232], # (Very Light Pastel Blue) 'Building'
268
- 14: [250, 244, 227], # (Cream) 'No Data'
269
- }
270
-
271
- elif color_scheme == 'dark_mode':
272
- return {
273
- -99: [0, 0, 0], # void
274
- -30: [153, 51, 102], # (Dark Pink) 'Landmark'
275
- -17: [76, 76, 76], # (Dark Gray) 'plaster'
276
- -16: [33, 46, 51], # (Very Dark Blue) 'glass'
277
- -15: [89, 84, 66], # (Very Dark Brown) 'stone'
278
- -14: [83, 89, 94], # (Dark Gray) 'metal'
279
- -13: [61, 61, 61], # (Dark Gray) 'concrete'
280
- -12: [153, 102, 0], # (Dark Orange) 'wood'
281
- -11: [51, 35, 33], # (Very Dark Red) 'brick'
282
- -3: [78, 82, 99], # (Dark Purple) Building
283
- -2: [46, 58, 37], # (Dark Green) Tree
284
- -1: [99, 68, 68], # (Dark Pink) Underground
285
- 1: [102, 97, 75], # (Dark Yellow) 'Bareland'
286
- 2: [61, 66, 31], # (Dark Olive) 'Rangeland'
287
- 3: [46, 77, 46], # (Dark Green) 'Shrub'
288
- 4: [56, 61, 28], # (Dark Yellow-Green) 'Agriculture land'
289
- 5: [54, 77, 31], # (Dark Green) 'Tree'
290
- 6: [89, 97, 20], # (Dark Yellow) 'Moss and lichen'
291
- 7: [38, 59, 49], # (Dark Teal) 'Wet land'
292
- 8: [16, 31, 26], # (Very Dark Green) 'Mangrove'
293
- 9: [22, 33, 66], # (Dark Blue) 'Water'
294
- 10: [82, 87, 92], # (Dark Blue-Gray) 'Snow and ice'
295
- 11: [46, 51, 56], # (Dark Gray) 'Developed space'
296
- 12: [25, 31, 43], # (Very Dark Blue) 'Road'
297
- 13: [56, 64, 82], # (Dark Blue-Gray) 'Building'
298
- 14: [102, 97, 75], # (Dark Yellow) 'No Data'
299
- }
300
-
301
- elif color_scheme == 'grayscale':
302
- return {
303
- -99: [0, 0, 0], # void (black)
304
- -30: [255, 0, 102], # (Pink) 'Landmark',
305
- -17: [240, 240, 240], # 'plaster'
306
- -16: [60, 60, 60], # 'glass'
307
- -15: [130, 130, 130], # 'stone'
308
- -14: [150, 150, 150], # 'metal'
309
- -13: [180, 180, 180], # 'concrete'
310
- -12: [170, 170, 170], # 'wood'
311
- -11: [70, 70, 70], # 'brick'
312
- -3: [190, 190, 190], # Building
313
- -2: [90, 90, 90], # Tree
314
- -1: [160, 160, 160], # Underground
315
- 1: [230, 230, 230], # 'Bareland'
316
- 2: [120, 120, 120], # 'Rangeland'
317
- 3: [110, 110, 110], # 'Shrub'
318
- 4: [115, 115, 115], # 'Agriculture land'
319
- 5: [100, 100, 100], # 'Tree'
320
- 6: [210, 210, 210], # 'Moss and lichen'
321
- 7: [95, 95, 95], # 'Wet land'
322
- 8: [40, 40, 40], # 'Mangrove'
323
- 9: [50, 50, 50], # 'Water'
324
- 10: [220, 220, 220], # 'Snow and ice'
325
- 11: [140, 140, 140], # 'Developed space'
326
- 12: [30, 30, 30], # 'Road'
327
- 13: [170, 170, 170], # 'Building'
328
- 14: [230, 230, 230], # 'No Data'
329
- }
330
-
331
- elif color_scheme == 'autumn':
332
- return {
333
- -99: [0, 0, 0], # void
334
- -30: [227, 66, 52], # (Red) 'Landmark'
335
- -17: [250, 240, 230], # (Antique White) 'plaster'
336
- -16: [94, 33, 41], # (Dark Red) 'glass'
337
- -15: [160, 120, 90], # (Medium Brown) 'stone'
338
- -14: [176, 141, 87], # (Bronze) 'metal'
339
- -13: [205, 186, 150], # (Tan) 'concrete'
340
- -12: [204, 85, 0], # (Dark Orange) 'wood'
341
- -11: [128, 55, 36], # (Rust) 'brick'
342
- -3: [222, 184, 135], # (Tan) Building
343
- -2: [107, 68, 35], # (Brown) Tree
344
- -1: [165, 105, 79], # (Copper) Underground
345
- 1: [255, 235, 205], # (Blanched Almond) 'Bareland'
346
- 2: [133, 99, 99], # (Brown) 'Rangeland'
347
- 3: [139, 69, 19], # (Saddle Brown) 'Shrub'
348
- 4: [160, 82, 45], # (Sienna) 'Agriculture land'
349
- 5: [101, 67, 33], # (Dark Brown) 'Tree'
350
- 6: [255, 228, 196], # (Bisque) 'Moss and lichen'
351
- 7: [138, 51, 36], # (Rust) 'Wet land'
352
- 8: [85, 45, 23], # (Deep Brown) 'Mangrove'
353
- 9: [175, 118, 70], # (Light Brown) 'Water'
354
- 10: [255, 250, 240], # (Floral White) 'Snow and ice'
355
- 11: [188, 143, 143], # (Rosy Brown) 'Developed space'
356
- 12: [69, 41, 33], # (Very Dark Brown) 'Road'
357
- 13: [210, 180, 140], # (Tan) 'Building'
358
- 14: [255, 235, 205], # (Blanched Almond) 'No Data'
359
- }
360
-
361
- elif color_scheme == 'cool':
362
- return {
363
- -99: [0, 0, 0], # void
364
- -30: [180, 82, 205], # (Purple) 'Landmark'
365
- -17: [240, 248, 255], # (Alice Blue) 'plaster'
366
- -16: [70, 130, 180], # (Steel Blue) 'glass'
367
- -15: [100, 149, 237], # (Cornflower Blue) 'stone'
368
- -14: [176, 196, 222], # (Light Steel Blue) 'metal'
369
- -13: [240, 255, 255], # (Azure) 'concrete'
370
- -12: [65, 105, 225], # (Royal Blue) 'wood'
371
- -11: [95, 158, 160], # (Cadet Blue) 'brick'
372
- -3: [135, 206, 235], # (Sky Blue) Building
373
- -2: [0, 128, 128], # (Teal) Tree
374
- -1: [127, 255, 212], # (Aquamarine) Underground
375
- 1: [220, 240, 250], # (Light Blue) 'Bareland'
376
- 2: [72, 209, 204], # (Medium Turquoise) 'Rangeland'
377
- 3: [0, 191, 255], # (Deep Sky Blue) 'Shrub'
378
- 4: [100, 149, 237], # (Cornflower Blue) 'Agriculture land'
379
- 5: [0, 128, 128], # (Teal) 'Tree'
380
- 6: [175, 238, 238], # (Pale Turquoise) 'Moss and lichen'
381
- 7: [32, 178, 170], # (Light Sea Green) 'Wet land'
382
- 8: [25, 25, 112], # (Midnight Blue) 'Mangrove'
383
- 9: [30, 144, 255], # (Dodger Blue) 'Water'
384
- 10: [240, 255, 255], # (Azure) 'Snow and ice'
385
- 11: [119, 136, 153], # (Light Slate Gray) 'Developed space'
386
- 12: [25, 25, 112], # (Midnight Blue) 'Road'
387
- 13: [173, 216, 230], # (Light Blue) 'Building'
388
- 14: [220, 240, 250], # (Light Blue) 'No Data'
389
- }
390
-
391
- elif color_scheme == 'earth_tones':
392
- return {
393
- -99: [0, 0, 0], # void
394
- -30: [210, 105, 30], # (Chocolate) 'Landmark'
395
- -17: [245, 245, 220], # (Beige) 'plaster'
396
- -16: [139, 137, 137], # (Gray) 'glass'
397
- -15: [160, 120, 90], # (Medium Brown) 'stone'
398
- -14: [169, 169, 169], # (Dark Gray) 'metal'
399
- -13: [190, 190, 180], # (Light Gray-Tan) 'concrete'
400
- -12: [160, 82, 45], # (Sienna) 'wood'
401
- -11: [139, 69, 19], # (Saddle Brown) 'brick'
402
- -3: [210, 180, 140], # (Tan) Building
403
- -2: [85, 107, 47], # (Dark Olive Green) Tree
404
- -1: [133, 94, 66], # (Beaver) Underground
405
- 1: [222, 184, 135], # (Burlywood) 'Bareland'
406
- 2: [107, 142, 35], # (Olive Drab) 'Rangeland'
407
- 3: [85, 107, 47], # (Dark Olive Green) 'Shrub'
408
- 4: [128, 128, 0], # (Olive) 'Agriculture land'
409
- 5: [34, 139, 34], # (Forest Green) 'Tree'
410
- 6: [189, 183, 107], # (Dark Khaki) 'Moss and lichen'
411
- 7: [143, 188, 143], # (Dark Sea Green) 'Wet land'
412
- 8: [46, 139, 87], # (Sea Green) 'Mangrove'
413
- 9: [95, 158, 160], # (Cadet Blue) 'Water'
414
- 10: [238, 232, 205], # (Light Tan) 'Snow and ice'
415
- 11: [169, 169, 169], # (Dark Gray) 'Developed space'
416
- 12: [90, 90, 90], # (Dark Gray) 'Road'
417
- 13: [188, 170, 152], # (Tan) 'Building'
418
- 14: [222, 184, 135], # (Burlywood) 'No Data'
419
- }
420
-
421
- elif color_scheme == 'vibrant':
422
- return {
423
- -99: [0, 0, 0], # void
424
- -30: [255, 0, 255], # (Magenta) 'Landmark'
425
- -17: [255, 255, 255], # (White) 'plaster'
426
- -16: [0, 191, 255], # (Deep Sky Blue) 'glass'
427
- -15: [255, 215, 0], # (Gold) 'stone'
428
- -14: [0, 250, 154], # (Medium Spring Green) 'metal'
429
- -13: [211, 211, 211], # (Light Gray) 'concrete'
430
- -12: [255, 69, 0], # (Orange Red) 'wood'
431
- -11: [178, 34, 34], # (Firebrick) 'brick'
432
- -3: [123, 104, 238], # (Medium Slate Blue) Building
433
- -2: [50, 205, 50], # (Lime Green) Tree
434
- -1: [255, 20, 147], # (Deep Pink) Underground
435
- 1: [255, 255, 0], # (Yellow) 'Bareland'
436
- 2: [0, 255, 0], # (Lime) 'Rangeland'
437
- 3: [0, 128, 0], # (Green) 'Shrub'
438
- 4: [154, 205, 50], # (Yellow Green) 'Agriculture land'
439
- 5: [34, 139, 34], # (Forest Green) 'Tree'
440
- 6: [127, 255, 0], # (Chartreuse) 'Moss and lichen'
441
- 7: [64, 224, 208], # (Turquoise) 'Wet land'
442
- 8: [0, 100, 0], # (Dark Green) 'Mangrove'
443
- 9: [0, 0, 255], # (Blue) 'Water'
444
- 10: [240, 248, 255], # (Alice Blue) 'Snow and ice'
445
- 11: [128, 128, 128], # (Gray) 'Developed space'
446
- 12: [47, 79, 79], # (Dark Slate Gray) 'Road'
447
- 13: [135, 206, 250], # (Light Sky Blue) 'Building'
448
- 14: [255, 255, 224], # (Light Yellow) 'No Data'
449
- }
450
-
451
- # ------------------------------------------------
452
- # NEWLY ADDED STYLISH COLOR SCHEMES BELOW:
453
- # ------------------------------------------------
454
- elif color_scheme == 'cyberpunk':
455
- """
456
- Vibrant neon purples, pinks, and blues with deep blacks.
457
- Think futuristic city vibes and bright neon signs.
458
- """
459
- return {
460
- -99: [0, 0, 0], # void (keep it pitch black)
461
- -30: [255, 0, 255], # (Neon Magenta) 'Landmark'
462
- -17: [255, 255, 255], # (Bright White) 'plaster'
463
- -16: [0, 255, 255], # (Neon Cyan) 'glass'
464
- -15: [128, 0, 128], # (Purple) 'stone'
465
- -14: [50, 50, 50], # (Dark Gray) 'metal'
466
- -13: [102, 0, 102], # (Dark Magenta) 'concrete'
467
- -12: [255, 20, 147], # (Deep Pink) 'wood'
468
- -11: [153, 0, 76], # (Deep Purple-Red) 'brick'
469
- -3: [124, 0, 255], # (Strong Neon Purple) Building
470
- -2: [0, 255, 153], # (Neon Greenish Cyan) Tree
471
- -1: [255, 0, 102], # (Hot Pink) Underground
472
- 1: [255, 255, 153], # (Pale Yellow) 'Bareland'
473
- 2: [0, 204, 204], # (Teal) 'Rangeland'
474
- 3: [153, 51, 255], # (Light Purple) 'Shrub'
475
- 4: [0, 153, 255], # (Bright Neon Blue) 'Agriculture land'
476
- 5: [0, 255, 153], # (Neon Greenish Cyan) 'Tree'
477
- 6: [204, 0, 255], # (Vivid Violet) 'Moss and lichen'
478
- 7: [0, 255, 255], # (Neon Cyan) 'Wet land'
479
- 8: [0, 102, 102], # (Dark Teal) 'Mangrove'
480
- 9: [51, 0, 102], # (Deep Indigo) 'Water'
481
- 10: [255, 255, 255], # (White) 'Snow and ice'
482
- 11: [102, 102, 102], # (Gray) 'Developed space'
483
- 12: [0, 0, 0], # (Black) 'Road'
484
- 13: [204, 51, 255], # (Bright Magenta) 'Building'
485
- 14: [255, 255, 153], # (Pale Yellow) 'No Data'
486
- }
487
-
488
- elif color_scheme == 'tropical':
489
- """
490
- Bold, bright 'tropical vacation' color palette.
491
- Lots of greens, oranges, pinks, reminiscent of island florals.
492
- """
493
- return {
494
- -99: [0, 0, 0], # void
495
- -30: [255, 99, 164], # (Bright Tropical Pink) 'Landmark'
496
- -17: [255, 248, 220], # (Cornsilk) 'plaster'
497
- -16: [0, 150, 136], # (Teal) 'glass'
498
- -15: [255, 140, 0], # (Dark Orange) 'stone'
499
- -14: [255, 215, 180], # (Light Peach) 'metal'
500
- -13: [210, 210, 210], # (Light Gray) 'concrete'
501
- -12: [255, 165, 0], # (Orange) 'wood'
502
- -11: [205, 92, 92], # (Indian Red) 'brick'
503
- -3: [255, 193, 37], # (Tropical Yellow) Building
504
- -2: [34, 139, 34], # (Forest Green) Tree
505
- -1: [255, 160, 122], # (Light Salmon) Underground
506
- 1: [240, 230, 140], # (Khaki) 'Bareland'
507
- 2: [60, 179, 113], # (Medium Sea Green) 'Rangeland'
508
- 3: [46, 139, 87], # (Sea Green) 'Shrub'
509
- 4: [255, 127, 80], # (Coral) 'Agriculture land'
510
- 5: [50, 205, 50], # (Lime Green) 'Tree'
511
- 6: [255, 239, 213], # (Papaya Whip) 'Moss and lichen'
512
- 7: [255, 99, 71], # (Tomato) 'Wet land'
513
- 8: [47, 79, 79], # (Dark Slate Gray) 'Mangrove'
514
- 9: [0, 128, 128], # (Teal) 'Water'
515
- 10: [224, 255, 255], # (Light Cyan) 'Snow and ice'
516
- 11: [218, 112, 214], # (Orchid) 'Developed space'
517
- 12: [85, 107, 47], # (Dark Olive Green) 'Road'
518
- 13: [253, 245, 230], # (Old Lace) 'Building'
519
- 14: [240, 230, 140], # (Khaki) 'No Data'
520
- }
521
-
522
- elif color_scheme == 'vintage':
523
- """
524
- A muted, old-photo or sepia-inspired palette
525
- for a nostalgic or antique look.
526
- """
527
- return {
528
- -99: [0, 0, 0], # void
529
- -30: [133, 94, 66], # (Beaver/Brownish) 'Landmark'
530
- -17: [250, 240, 230], # (Antique White) 'plaster'
531
- -16: [169, 157, 143], # (Muted Brown-Gray) 'glass'
532
- -15: [181, 166, 127], # (Khaki Tan) 'stone'
533
- -14: [120, 106, 93], # (Faded Gray-Brown) 'metal'
534
- -13: [190, 172, 145], # (Light Brown) 'concrete'
535
- -12: [146, 109, 83], # (Leather Brown) 'wood'
536
- -11: [125, 80, 70], # (Dusty Brick) 'brick'
537
- -3: [201, 174, 146], # (Tanned Beige) Building
538
- -2: [112, 98, 76], # (Faded Olive-Brown) Tree
539
- -1: [172, 140, 114], # (Light Saddle Brown) Underground
540
- 1: [222, 202, 166], # (Light Tan) 'Bareland'
541
- 2: [131, 114, 83], # (Brownish) 'Rangeland'
542
- 3: [105, 96, 74], # (Dark Olive Brown) 'Shrub'
543
- 4: [162, 141, 118], # (Beige Brown) 'Agriculture land'
544
- 5: [95, 85, 65], # (Muted Dark Brown) 'Tree'
545
- 6: [212, 200, 180], # (Off-White Tan) 'Moss and lichen'
546
- 7: [140, 108, 94], # (Dusky Mauve-Brown) 'Wet land'
547
- 8: [85, 73, 60], # (Dark Taupe) 'Mangrove'
548
- 9: [166, 152, 121], # (Pale Brown) 'Water'
549
- 10: [250, 245, 235], # (Light Antique White) 'Snow and ice'
550
- 11: [120, 106, 93], # (Faded Gray-Brown) 'Developed space'
551
- 12: [77, 66, 55], # (Dark Taupe) 'Road'
552
- 13: [203, 188, 162], # (Light Warm Gray) 'Building'
553
- 14: [222, 202, 166], # (Light Tan) 'No Data'
554
- }
555
-
556
- elif color_scheme == 'neon_dreams':
557
- """
558
- A super-bright, high-energy neon palette.
559
- Perfect if you want a 'nightclub in 2080' vibe.
560
- """
561
- return {
562
- -99: [0, 0, 0], # void
563
- -30: [255, 0, 255], # (Magenta) 'Landmark'
564
- -17: [255, 255, 255], # (White) 'plaster'
565
- -16: [0, 255, 255], # (Cyan) 'glass'
566
- -15: [255, 255, 0], # (Yellow) 'stone'
567
- -14: [0, 255, 0], # (Lime) 'metal'
568
- -13: [128, 128, 128], # (Gray) 'concrete'
569
- -12: [255, 165, 0], # (Neon Orange) 'wood'
570
- -11: [255, 20, 147], # (Deep Pink) 'brick'
571
- -3: [75, 0, 130], # (Indigo) Building
572
- -2: [102, 255, 0], # (Bright Lime Green) Tree
573
- -1: [255, 51, 153], # (Neon Pink) Underground
574
- 1: [255, 153, 0], # (Bright Orange) 'Bareland'
575
- 2: [153, 204, 0], # (Vivid Yellow-Green) 'Rangeland'
576
- 3: [102, 205, 170], # (Aquamarine-ish) 'Shrub'
577
- 4: [0, 250, 154], # (Medium Spring Green) 'Agriculture land'
578
- 5: [173, 255, 47], # (Green-Yellow) 'Tree'
579
- 6: [127, 255, 0], # (Chartreuse) 'Moss and lichen'
580
- 7: [64, 224, 208], # (Turquoise) 'Wet land'
581
- 8: [0, 128, 128], # (Teal) 'Mangrove'
582
- 9: [0, 0, 255], # (Blue) 'Water'
583
- 10: [224, 255, 255], # (Light Cyan) 'Snow and ice'
584
- 11: [192, 192, 192], # (Silver) 'Developed space'
585
- 12: [25, 25, 25], # (Near Black) 'Road'
586
- 13: [75, 0, 130], # (Indigo) 'Building'
587
- 14: [255, 153, 0], # (Bright Orange) 'No Data'
588
- }
589
-
590
- else:
591
- # If an unknown color scheme is specified, return the default
592
- print(f"Unknown color scheme '{color_scheme}'. Using default instead.")
593
- return get_voxel_color_map('default')
594
-
595
- def visualize_3d_voxel(voxel_grid, voxel_color_map = 'default', voxel_size=2.0, save_path=None):
596
- """
597
- Visualizes 3D voxel data using matplotlib's 3D plotting capabilities.
598
-
599
- This function creates a 3D visualization of voxel data where each non-zero voxel
600
- is rendered as a colored cube. The colors are determined by the voxel values
601
- and the specified color scheme. The visualization includes proper transparency
602
- handling and aspect ratio adjustment.
603
-
604
- Parameters:
605
- -----------
606
- voxel_grid : numpy.ndarray
607
- 3D numpy array containing voxel data. Shape should be (x, y, z) where
608
- each element represents a voxel class ID. Zero values are treated as empty space.
609
-
610
- voxel_color_map : str, optional
611
- Name of the color scheme to use for voxel coloring. Default is 'default'.
612
- See get_voxel_color_map() for available options.
613
-
614
- voxel_size : float, optional
615
- Physical size of each voxel in meters. Used for z-axis scaling and labels.
616
- Default is 2.0 meters.
617
-
618
- save_path : str, optional
619
- File path to save the generated plot. If None, the plot is only displayed.
620
- Default is None.
621
-
622
- Returns:
623
- --------
624
- None
625
- The function displays the plot and optionally saves it to file.
626
-
627
- Notes:
628
- ------
629
- - Void voxels (value -99) are rendered transparent
630
- - Underground voxels (value -1) and trees (value -2) have reduced transparency
631
- - Z-axis ticks are automatically scaled to show real-world heights
632
- - The plot aspect ratio is adjusted to maintain proper voxel proportions
633
- - For large voxel grids, this function may be slow due to matplotlib's 3D rendering
634
-
635
- Examples:
636
- ---------
637
- >>> # Basic visualization
638
- >>> visualize_3d_voxel(voxel_array)
639
-
640
- >>> # Use cyberpunk color scheme and save to file
641
- >>> visualize_3d_voxel(voxel_array, 'cyberpunk', save_path='city_view.png')
642
-
643
- >>> # Adjust voxel size for different scale
644
- >>> visualize_3d_voxel(voxel_array, voxel_size=1.0)
645
- """
646
- # Get the color mapping for the specified scheme
647
- color_map = get_voxel_color_map(voxel_color_map)
648
-
649
- print("\tVisualizing 3D voxel data")
650
- # Create a figure and a 3D axis
651
- fig = plt.figure(figsize=(12, 10))
652
- ax = fig.add_subplot(111, projection='3d')
653
-
654
- print("\tProcessing voxels...")
655
- # Create boolean mask for voxels that should be rendered (non-zero values)
656
- filled_voxels = voxel_grid != 0
657
-
658
- # Initialize color array with RGBA values (Red, Green, Blue, Alpha)
659
- colors = np.zeros(voxel_grid.shape + (4,)) # RGBA
660
-
661
- # Process each possible voxel value and assign colors
662
- for val in range(-99, 15): # Updated range to include -3 and -2
663
- # Create mask for voxels with this specific value
664
- mask = voxel_grid == val
665
-
666
- if val in color_map:
667
- # Convert RGB values from [0,255] to [0,1] range for matplotlib
668
- rgb = [x/255 for x in color_map[val]] # Normalize RGB values to [0, 1]
669
-
670
- # Set transparency based on voxel type
671
- # alpha = 0.7 if ((val == -1) or (val == -2)) else 0.9 # More transparent for underground and below
672
- alpha = 0.0 if (val == -99) else 1 # Void voxels are completely transparent
673
- # alpha = 1
674
-
675
- # Assign RGBA color to all voxels of this type
676
- colors[mask] = rgb + [alpha]
677
- else:
678
- # Default color for undefined voxel types
679
- colors[mask] = [0, 0, 0, 0.9] # Default color if not in color_map
680
-
681
- # Render voxels with progress bar
682
- with tqdm(total=np.prod(voxel_grid.shape)) as pbar:
683
- ax.voxels(filled_voxels, facecolors=colors, edgecolors=None)
684
- pbar.update(np.prod(voxel_grid.shape))
685
-
686
- # print("Finalizing plot...")
687
- # Set labels and title
688
- # ax.set_xlabel('X')
689
- # ax.set_ylabel('Y')
690
- # ax.set_zlabel('Z (meters)')
691
- # ax.set_title('3D Voxel Visualization')
692
-
693
- # Configure z-axis ticks to show meaningful height values
694
- # Adjust z-axis ticks to show every 10 cells or less
695
- z_max = voxel_grid.shape[2]
696
- if z_max <= 10:
697
- z_ticks = range(0, z_max + 1)
698
- else:
699
- z_ticks = range(0, z_max + 1, 10)
700
-
701
- # Remove axes for cleaner appearance
702
- ax.axis('off')
703
- # ax.set_zticks(z_ticks)
704
- # ax.set_zticklabels([f"{z * voxel_size:.1f}" for z in z_ticks])
705
-
706
- # Set aspect ratio to be equal for realistic proportions
707
- max_range = np.array([voxel_grid.shape[0], voxel_grid.shape[1], voxel_grid.shape[2]]).max()
708
- ax.set_box_aspect((voxel_grid.shape[0]/max_range, voxel_grid.shape[1]/max_range, voxel_grid.shape[2]/max_range))
709
-
710
- # Set z-axis limits to focus on the relevant height range
711
- ax.set_zlim(bottom=0)
712
- ax.set_zlim(top=150)
713
-
714
- # print("Visualization complete. Displaying plot...")
715
- plt.tight_layout()
716
-
717
- # Save plot if path is provided
718
- if save_path:
719
- plt.savefig(save_path, dpi=300, bbox_inches='tight')
720
- plt.show()
721
-
722
- def visualize_3d_voxel_plotly(voxel_grid, voxel_color_map = 'default', voxel_size=2.0):
723
- """
724
- Creates an interactive 3D visualization of voxel data using Plotly.
725
-
726
- This function generates an interactive 3D visualization using Plotly's Mesh3d
727
- and Scatter3d objects. Each voxel is rendered as a cube with proper lighting
728
- and edge visualization. The resulting plot supports interactive rotation,
729
- zooming, and panning.
730
-
731
- Parameters:
732
- -----------
733
- voxel_grid : numpy.ndarray
734
- 3D numpy array containing voxel data. Shape should be (x, y, z) where
735
- each element represents a voxel class ID. Zero values are treated as empty space.
736
-
737
- voxel_color_map : str, optional
738
- Name of the color scheme to use for voxel coloring. Default is 'default'.
739
- See get_voxel_color_map() for available options.
740
-
741
- voxel_size : float, optional
742
- Physical size of each voxel in meters. Used for z-axis scaling and labels.
743
- Default is 2.0 meters.
744
-
745
- Returns:
746
- --------
747
- None
748
- The function displays the interactive plot in the browser or notebook.
749
-
750
- Notes:
751
- ------
752
- - Creates individual cube geometries for each non-zero voxel
753
- - Includes edge lines for better visual definition
754
- - Uses orthographic projection for technical visualization
755
- - May be slow for very large voxel grids due to individual cube generation
756
- - Lighting is optimized for clear visualization of building structures
757
-
758
- Technical Details:
759
- ------------------
760
- - Each cube is defined by 8 vertices and 12 triangular faces
761
- - Edge lines are generated separately for visual clarity
762
- - Color mapping follows the same convention as matplotlib version
763
- - Camera is positioned for isometric view with orthographic projection
764
-
765
- Examples:
766
- ---------
767
- >>> # Basic interactive visualization
768
- >>> visualize_3d_voxel_plotly(voxel_array)
769
-
770
- >>> # Use high contrast colors for better visibility
771
- >>> visualize_3d_voxel_plotly(voxel_array, 'high_contrast')
772
-
773
- >>> # Adjust scale for different voxel sizes
774
- >>> visualize_3d_voxel_plotly(voxel_array, voxel_size=1.0)
775
- """
776
- # Get the color mapping for the specified scheme
777
- color_map = get_voxel_color_map(voxel_color_map)
778
-
779
- print("Preparing visualization...")
780
-
781
- print("Processing voxels...")
782
- # Initialize lists to store mesh data
783
- x, y, z = [], [], [] # Vertex coordinates
784
- i, j, k = [], [], [] # Face indices (triangles)
785
- colors = [] # Vertex colors
786
- edge_x, edge_y, edge_z = [], [], [] # Edge line coordinates
787
- vertex_index = 0 # Current vertex index counter
788
-
789
- # Define cube faces using vertex indices
790
- # Each cube has 12 triangular faces (2 per square face)
791
- cube_i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
792
- cube_j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
793
- cube_k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
794
-
795
- # Process each voxel in the grid
796
- with tqdm(total=np.prod(voxel_grid.shape)) as pbar:
797
- for xi in range(voxel_grid.shape[0]):
798
- for yi in range(voxel_grid.shape[1]):
799
- for zi in range(voxel_grid.shape[2]):
800
- # Only process non-zero voxels
801
- if voxel_grid[xi, yi, zi] != 0:
802
- # Define the 8 vertices of a unit cube at this position
803
- # Vertices are ordered: bottom face (z), then top face (z+1)
804
- cube_vertices = [
805
- [xi, yi, zi], [xi+1, yi, zi], [xi+1, yi+1, zi], [xi, yi+1, zi], # Bottom face
806
- [xi, yi, zi+1], [xi+1, yi, zi+1], [xi+1, yi+1, zi+1], [xi, yi+1, zi+1] # Top face
807
- ]
808
-
809
- # Add vertex coordinates to the mesh data
810
- x.extend([v[0] for v in cube_vertices])
811
- y.extend([v[1] for v in cube_vertices])
812
- z.extend([v[2] for v in cube_vertices])
813
-
814
- # Add face indices (offset by current vertex_index)
815
- i.extend([x + vertex_index for x in cube_i])
816
- j.extend([x + vertex_index for x in cube_j])
817
- k.extend([x + vertex_index for x in cube_k])
818
-
819
- # Get color for this voxel type and replicate for all 8 vertices
820
- color = color_map.get(voxel_grid[xi, yi, zi], [0, 0, 0])
821
- colors.extend([color] * 8)
822
-
823
- # Generate edge lines for visual clarity
824
- # Define the 12 edges of a cube (4 bottom + 4 top + 4 vertical)
825
- edges = [
826
- (0,1), (1,2), (2,3), (3,0), # Bottom face edges
827
- (4,5), (5,6), (6,7), (7,4), # Top face edges
828
- (0,4), (1,5), (2,6), (3,7) # Vertical edges
829
- ]
830
-
831
- # Add edge coordinates (None creates line breaks between edges)
832
- for start, end in edges:
833
- edge_x.extend([cube_vertices[start][0], cube_vertices[end][0], None])
834
- edge_y.extend([cube_vertices[start][1], cube_vertices[end][1], None])
835
- edge_z.extend([cube_vertices[start][2], cube_vertices[end][2], None])
836
-
837
- # Increment vertex index for next cube
838
- vertex_index += 8
839
- pbar.update(1)
840
-
841
- print("Creating Plotly figure...")
842
- # Create 3D mesh object with vertices, faces, and colors
843
- mesh = go.Mesh3d(
844
- x=x, y=y, z=z,
845
- i=i, j=j, k=k,
846
- vertexcolor=colors,
847
- opacity=1,
848
- flatshading=True,
849
- name='Voxel Grid'
850
- )
851
-
852
- # Configure lighting for better visualization
853
- # Add lighting to the mesh
854
- mesh.update(
855
- lighting=dict(ambient=0.7, # Ambient light (overall brightness)
856
- diffuse=1, # Diffuse light (surface shading)
857
- fresnel=0.1, # Fresnel effect (edge highlighting)
858
- specular=1, # Specular highlights
859
- roughness=0.05, # Surface roughness
860
- facenormalsepsilon=1e-15,
861
- vertexnormalsepsilon=1e-15),
862
- lightposition=dict(x=100, # Light source position
863
- y=200,
864
- z=0)
865
- )
866
-
867
- # Create edge lines for better visual definition
868
- edges = go.Scatter3d(
869
- x=edge_x, y=edge_y, z=edge_z,
870
- mode='lines',
871
- line=dict(color='lightgrey', width=1),
872
- name='Edges'
873
- )
874
-
875
- # Combine mesh and edges into a figure
876
- fig = go.Figure(data=[mesh, edges])
877
-
878
- # Configure plot layout and camera settings
879
- # Set labels, title, and use orthographic projection
880
- fig.update_layout(
881
- scene=dict(
882
- xaxis_title='X',
883
- yaxis_title='Y',
884
- zaxis_title='Z (meters)',
885
- aspectmode='data', # Maintain data aspect ratios
886
- camera=dict(
887
- projection=dict(type="orthographic") # Use orthographic projection
888
- )
889
- ),
890
- title='3D Voxel Visualization'
891
- )
892
-
893
- # Configure z-axis to show real-world heights
894
- # Adjust z-axis ticks to show every 10 cells or less
895
- z_max = voxel_grid.shape[2]
896
- if z_max <= 10:
897
- z_ticks = list(range(0, z_max + 1))
898
- else:
899
- z_ticks = list(range(0, z_max + 1, 10))
900
-
901
- # Update z-axis with meaningful height labels
902
- fig.update_layout(
903
- scene=dict(
904
- zaxis=dict(
905
- tickvals=z_ticks,
906
- ticktext=[f"{z * voxel_size:.1f}" for z in z_ticks]
907
- )
908
- )
909
- )
910
-
911
- print("Visualization complete. Displaying plot...")
912
- fig.show()
913
-
914
- def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light', **kwargs):
915
- """
916
- Core function for plotting 2D grid data overlaid on basemaps.
917
-
918
- This function handles the visualization of various types of grid data by creating
919
- colored polygons for each grid cell and overlaying them on a web basemap. It supports
920
- different data types with appropriate color schemes and handles special values like
921
- NaN and zero appropriately.
922
-
923
- Parameters:
924
- -----------
925
- grid : numpy.ndarray
926
- 2D array containing the grid data values to visualize.
927
-
928
- origin : numpy.ndarray
929
- Geographic coordinates [lon, lat] of the grid's origin point.
930
-
931
- adjusted_meshsize : float
932
- Size of each grid cell in meters after grid size adjustments.
933
-
934
- u_vec, v_vec : numpy.ndarray
935
- Unit vectors defining the grid orientation in geographic space.
936
-
937
- transformer : pyproj.Transformer
938
- Coordinate transformer for converting between geographic and projected coordinates.
939
-
940
- vertices : list
941
- List of [lon, lat] coordinates defining the grid boundary.
942
-
943
- data_type : str
944
- Type of data being visualized. Supported types:
945
- - 'land_cover': Land use/land cover classifications
946
- - 'building_height': Building height values
947
- - 'dem': Digital elevation model
948
- - 'canopy_height': Vegetation height
949
- - 'green_view_index': Green visibility index
950
- - 'sky_view_index': Sky visibility factor
951
-
952
- vmin, vmax : float, optional
953
- Min/max values for color scaling. Auto-calculated if not provided.
954
-
955
- color_map : str, optional
956
- Matplotlib colormap name to override default schemes.
957
-
958
- alpha : float, optional
959
- Transparency of grid overlay (0-1). Default is 0.5.
960
-
961
- buf : float, optional
962
- Buffer around grid for plot extent as fraction of grid size. Default is 0.2.
963
-
964
- edge : bool, optional
965
- Whether to draw cell edges. Default is True.
966
-
967
- basemap : str, optional
968
- Basemap style name. Default is 'CartoDB light'.
969
-
970
- **kwargs : dict
971
- Additional parameters specific to data types:
972
- - land_cover_classes: Dictionary for land cover data
973
- - buildings: List of building polygons for building_height data
974
-
975
- Returns:
976
- --------
977
- None
978
- Displays the plot with matplotlib.
979
-
980
- Notes:
981
- ------
982
- - Grid is transposed to match geographic orientation
983
- - Special handling for NaN, zero, and negative values depending on data type
984
- - Basemap is added using contextily for geographic context
985
- - Plot extent is automatically calculated from grid vertices
986
- """
987
- # Create matplotlib figure and axis
988
- fig, ax = plt.subplots(figsize=(12, 12))
989
-
990
- # Configure visualization parameters based on data type
991
- if data_type == 'land_cover':
992
- # Land cover uses discrete color categories
993
- land_cover_classes = kwargs.get('land_cover_classes')
994
- colors = [mcolors.to_rgb(f'#{r:02x}{g:02x}{b:02x}') for r, g, b in land_cover_classes.keys()]
995
- cmap = mcolors.ListedColormap(colors)
996
- norm = mcolors.BoundaryNorm(range(len(land_cover_classes)+1), cmap.N)
997
- title = 'Grid Cells with Dominant Land Cover Classes'
998
- label = 'Land Cover Class'
999
- tick_labels = list(land_cover_classes.values())
1000
-
1001
- elif data_type == 'building_height':
1002
- # Building height uses continuous colormap with special handling for zero/NaN
1003
- # Create a masked array to handle special values
1004
- masked_grid = np.ma.masked_array(grid, mask=(np.isnan(grid) | (grid == 0)))
1005
-
1006
- # Set up colormap and normalization for positive values
1007
- cmap = plt.cm.viridis
1008
- if vmin is None:
1009
- vmin = np.nanmin(masked_grid[masked_grid > 0])
1010
- if vmax is None:
1011
- vmax = np.nanmax(masked_grid)
1012
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1013
-
1014
- title = 'Grid Cells with Building Heights'
1015
- label = 'Building Height (m)'
1016
- tick_labels = None
1017
-
1018
- elif data_type == 'dem':
1019
- # Digital elevation model uses terrain colormap
1020
- cmap = plt.cm.terrain
1021
- if vmin is None:
1022
- vmin = np.nanmin(grid)
1023
- if vmax is None:
1024
- vmax = np.nanmax(grid)
1025
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1026
- title = 'DEM Grid Overlaid on Map'
1027
- label = 'Elevation (m)'
1028
- tick_labels = None
1029
- elif data_type == 'canopy_height':
1030
- cmap = plt.cm.Greens
1031
- if vmin is None:
1032
- vmin = np.nanmin(grid)
1033
- if vmax is None:
1034
- vmax = np.nanmax(grid)
1035
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1036
- title = 'Canopy Height Grid Overlaid on Map'
1037
- label = 'Canopy Height (m)'
1038
- tick_labels = None
1039
- elif data_type == 'green_view_index':
1040
- cmap = plt.cm.Greens
1041
- if vmin is None:
1042
- vmin = np.nanmin(grid)
1043
- if vmax is None:
1044
- vmax = np.nanmax(grid)
1045
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1046
- title = 'Green View Index Grid Overlaid on Map'
1047
- label = 'Green View Index'
1048
- tick_labels = None
1049
- elif data_type == 'sky_view_index':
1050
- cmap = plt.cm.get_cmap('BuPu_r').copy()
1051
- if vmin is None:
1052
- vmin = np.nanmin(grid)
1053
- if vmax is None:
1054
- vmax = np.nanmax(grid)
1055
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1056
- title = 'Sky View Index Grid Overlaid on Map'
1057
- label = 'Sky View Index'
1058
- tick_labels = None
1059
- else:
1060
- cmap = plt.cm.viridis
1061
- if vmin is None:
1062
- vmin = np.nanmin(grid)
1063
- if vmax is None:
1064
- vmax = np.nanmax(grid)
1065
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1066
- tick_labels = None
1067
-
1068
- if color_map:
1069
- # cmap = plt.cm.get_cmap(color_map).copy()
1070
- cmap = sns.color_palette(color_map, as_cmap=True).copy()
1071
-
1072
- # Ensure grid is in the correct orientation
1073
- grid = grid.T
1074
-
1075
- for i in range(grid.shape[0]):
1076
- for j in range(grid.shape[1]):
1077
- cell = create_cell_polygon(origin, j, i, adjusted_meshsize, u_vec, v_vec) # Note the swap of i and j
1078
- x, y = cell.exterior.xy
1079
- x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
1080
-
1081
- value = grid[i, j]
1082
-
1083
- if data_type == 'building_height':
1084
- if np.isnan(value):
1085
- # White fill for NaN values
1086
- ax.fill(x, y, alpha=alpha, fc='gray', ec='black' if edge else None, linewidth=0.1)
1087
- elif value == 0:
1088
- # No fill for zero values, only edges if enabled
1089
- if edge:
1090
- ax.plot(x, y, color='black', linewidth=0.1)
1091
- elif value > 0:
1092
- # Viridis colormap for positive values
1093
- color = cmap(norm(value))
1094
- ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
1095
- elif data_type == 'canopy_height':
1096
- color = cmap(norm(value))
1097
- if value == 0:
1098
- # No fill for zero values, only edges if enabled
1099
- if edge:
1100
- ax.plot(x, y, color='black', linewidth=0.1)
1101
- else:
1102
- if edge:
1103
- ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
1104
- else:
1105
- ax.fill(x, y, alpha=alpha, fc=color, ec=None)
1106
- elif 'view' in data_type:
1107
- if np.isnan(value):
1108
- # No fill for zero values, only edges if enabled
1109
- if edge:
1110
- ax.plot(x, y, color='black', linewidth=0.1)
1111
- elif value >= 0:
1112
- # Viridis colormap for positive values
1113
- color = cmap(norm(value))
1114
- ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
1115
- else:
1116
- color = cmap(norm(value))
1117
- if edge:
1118
- ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
1119
- else:
1120
- ax.fill(x, y, alpha=alpha, fc=color, ec=None)
1121
-
1122
- crs_epsg_3857 = CRS.from_epsg(3857)
1123
-
1124
- basemaps = {
1125
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter, # Popular dark option
1126
- 'CartoDB light': ctx.providers.CartoDB.Positron, # Popular dark option
1127
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager, # Popular dark option
1128
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels, # Popular dark option
1129
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1130
- }
1131
- ctx.add_basemap(ax, crs=crs_epsg_3857, source=basemaps[basemap])
1132
- # if basemap == "dark":
1133
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.DarkMatter)
1134
- # elif basemap == 'light':
1135
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.Positron)
1136
- # elif basemap == 'voyager':
1137
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.Voyager)
1138
-
1139
- if data_type == 'building_height':
1140
- buildings = kwargs.get('buildings', [])
1141
- for building in buildings:
1142
- polygon = Polygon(building['geometry']['coordinates'][0])
1143
- x, y = polygon.exterior.xy
1144
- x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
1145
- ax.plot(x, y, color='red', linewidth=1.5)
1146
- # print(polygon)
1147
-
1148
- # Safe calculation of plot limits
1149
- all_coords = np.array(vertices)
1150
- x, y = zip(*[transformer.transform(lon, lat) for lat, lon in all_coords])
1151
-
1152
- # Calculate limits safely
1153
- x_min, x_max = min(x), max(x)
1154
- y_min, y_max = min(y), max(y)
1155
-
1156
- if x_min != x_max and y_min != y_max and buf != 0:
1157
- dist_x = x_max - x_min
1158
- dist_y = y_max - y_min
1159
- # Set limits with buffer
1160
- ax.set_xlim(x_min - buf * dist_x, x_max + buf * dist_x)
1161
- ax.set_ylim(y_min - buf * dist_y, y_max + buf * dist_y)
1162
- else:
1163
- # If coordinates are the same or buffer is 0, set limits without buffer
1164
- ax.set_xlim(x_min, x_max)
1165
- ax.set_ylim(y_min, y_max)
1166
-
1167
- plt.axis('off')
1168
- plt.tight_layout()
1169
- plt.show()
1170
-
1171
- def display_builing_ids_on_map(building_geojson, rectangle_vertices):
1172
- """
1173
- Creates an interactive folium map displaying building footprints with selectable IDs.
1174
-
1175
- This function generates a web map showing building polygons within a circular area
1176
- around the center of the specified rectangle. Each building is labeled with its
1177
- ID and additional information, making it easy to identify specific buildings
1178
- for analysis or selection.
1179
-
1180
- Parameters:
1181
- -----------
1182
- building_geojson : list
1183
- List of GeoJSON feature dictionaries representing building polygons.
1184
- Each feature should have:
1185
- - 'geometry': GeoJSON polygon geometry
1186
- - 'properties': Dictionary with 'id' and optional 'name' fields
1187
-
1188
- rectangle_vertices : list
1189
- List of [lat, lon] coordinate pairs defining the area of interest.
1190
- Used to calculate the map center and intersection area.
1191
-
1192
- Returns:
1193
- --------
1194
- folium.Map
1195
- Interactive folium map object with building polygons and labels.
1196
-
1197
- Notes:
1198
- ------
1199
- - Only buildings intersecting with a 200m radius circle are displayed
1200
- - Building IDs are displayed as selectable text labels
1201
- - Map is automatically centered on the rectangle vertices
1202
- - Popup information includes building ID and name (if available)
1203
- - Building polygons are styled with blue color and semi-transparent fill
1204
-
1205
- Examples:
1206
- ---------
1207
- >>> vertices = [[40.7580, -73.9855], [40.7590, -73.9855],
1208
- ... [40.7590, -73.9845], [40.7580, -73.9845]]
1209
- >>> buildings = [{'geometry': {...}, 'properties': {'id': '123', 'name': 'Building A'}}]
1210
- >>> map_obj = display_builing_ids_on_map(buildings, vertices)
1211
- >>> map_obj.save('buildings_map.html')
1212
- """
1213
- # Parse the GeoJSON data
1214
- geojson_data = building_geojson
1215
-
1216
- # Calculate the center point of the rectangle for map centering
1217
- # Extract all latitudes and longitudes
1218
- lats = [coord[0] for coord in rectangle_vertices]
1219
- lons = [coord[1] for coord in rectangle_vertices]
1220
-
1221
- # Calculate center by averaging min and max values
1222
- center_lat = (min(lats) + max(lats)) / 2
1223
- center_lon = (min(lons) + max(lons)) / 2
1224
-
1225
- # Create circle polygon for intersection testing (200m radius)
1226
- circle = create_circle_polygon(center_lat, center_lon, 200)
1227
-
1228
- # Create a map centered on the data
1229
- m = folium.Map(location=[center_lat, center_lon], zoom_start=17)
1230
-
1231
- # Process each building feature
1232
- # Add building footprints to the map
1233
- for feature in geojson_data:
1234
- # Convert coordinates if needed
1235
- coords = convert_coordinates(feature['geometry']['coordinates'][0])
1236
- building_polygon = Polygon(coords)
1237
-
1238
- # Only process buildings that intersect with the circular area
1239
- # Check if building intersects with circle
1240
- if building_polygon.intersects(circle):
1241
- # Extract building information from properties
1242
- # Get and format building properties
1243
- # building_id = format_building_id(feature['properties'].get('id', 0))
1244
- building_id = str(feature['properties'].get('id', 0))
1245
- building_name = feature['properties'].get('name:en',
1246
- feature['properties'].get('name', f'Building {building_id}'))
1247
-
1248
- # Create popup content with selectable ID
1249
- popup_content = f"""
1250
- <div>
1251
- Building ID: <span style="user-select: all">{building_id}</span><br>
1252
- Name: {building_name}
1253
- </div>
1254
- """
1255
-
1256
- # Add building polygon to map with popup information
1257
- # Add polygon to map
1258
- folium.Polygon(
1259
- locations=coords,
1260
- popup=folium.Popup(popup_content),
1261
- color='blue',
1262
- weight=2,
1263
- fill=True,
1264
- fill_color='blue',
1265
- fill_opacity=0.2
1266
- ).add_to(m)
1267
-
1268
- # Add building ID label at the polygon centroid
1269
- # Calculate centroid for label placement
1270
- centroid = calculate_centroid(coords)
1271
-
1272
- # Add building ID as a selectable label
1273
- folium.Marker(
1274
- centroid,
1275
- icon=folium.DivIcon(
1276
- html=f'''
1277
- <div style="
1278
- position: relative;
1279
- font-family: monospace;
1280
- font-size: 12px;
1281
- color: black;
1282
- background-color: rgba(255, 255, 255, 0.9);
1283
- padding: 5px 8px;
1284
- margin: -10px -15px;
1285
- border: 1px solid black;
1286
- border-radius: 4px;
1287
- user-select: all;
1288
- cursor: text;
1289
- white-space: nowrap;
1290
- display: inline-block;
1291
- box-shadow: 0 0 3px rgba(0,0,0,0.2);
1292
- ">{building_id}</div>
1293
- ''',
1294
- class_name="building-label"
1295
- )
1296
- ).add_to(m)
1297
-
1298
- # Save the map
1299
- return m
1300
-
1301
- def visualize_land_cover_grid_on_map(grid, rectangle_vertices, meshsize, source = 'Urbanwatch', vmin=None, vmax=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1302
- """
1303
- Visualizes land cover classification grid overlaid on a basemap.
1304
-
1305
- This function creates a map visualization of land cover data using predefined
1306
- color schemes for different land cover classes. Each grid cell is colored
1307
- according to its dominant land cover type and overlaid on a web basemap
1308
- for geographic context.
1309
-
1310
- Parameters:
1311
- -----------
1312
- grid : numpy.ndarray
1313
- 2D array containing land cover class indices. Values should correspond
1314
- to indices in the land cover classification system.
1315
-
1316
- rectangle_vertices : list
1317
- List of [lon, lat] coordinate pairs defining the grid boundary corners.
1318
- Should contain exactly 4 vertices in geographic coordinates.
1319
-
1320
- meshsize : float
1321
- Target size of each grid cell in meters. May be adjusted during processing.
1322
-
1323
- source : str, optional
1324
- Source of land cover classification system. Default is 'Urbanwatch'.
1325
- See get_land_cover_classes() for available options.
1326
-
1327
- vmin, vmax : float, optional
1328
- Not used for land cover (discrete categories). Included for API consistency.
1329
-
1330
- alpha : float, optional
1331
- Transparency of grid overlay (0-1). Default is 0.5.
1332
-
1333
- buf : float, optional
1334
- Buffer around grid for plot extent as fraction of grid size. Default is 0.2.
1335
-
1336
- edge : bool, optional
1337
- Whether to draw cell edges. Default is True.
1338
-
1339
- basemap : str, optional
1340
- Basemap style name. Options include:
1341
- - 'CartoDB light' (default)
1342
- - 'CartoDB dark'
1343
- - 'CartoDB voyager'
1344
- Default is 'CartoDB light'.
1345
-
1346
- Returns:
1347
- --------
1348
- None
1349
- Displays the plot and prints information about unique land cover classes.
1350
-
1351
- Notes:
1352
- ------
1353
- - Grid coordinates are calculated using geodetic calculations
1354
- - Land cover classes are mapped to predefined colors
1355
- - Unique classes present in the grid are printed for reference
1356
- - Uses Web Mercator projection (EPSG:3857) for basemap compatibility
1357
-
1358
- Examples:
1359
- ---------
1360
- >>> # Basic land cover visualization
1361
- >>> vertices = [[lon1, lat1], [lon2, lat2], [lon3, lat3], [lon4, lat4]]
1362
- >>> visualize_land_cover_grid_on_map(lc_grid, vertices, 10.0)
1363
-
1364
- >>> # With custom styling
1365
- >>> visualize_land_cover_grid_on_map(lc_grid, vertices, 10.0,
1366
- ... alpha=0.7, edge=False,
1367
- ... basemap='CartoDB dark')
1368
- """
1369
- # Initialize geodetic calculator for distance measurements
1370
- geod = initialize_geod()
1371
-
1372
- # Get land cover class definitions and colors
1373
- land_cover_classes = get_land_cover_classes(source)
1374
-
1375
- # Extract key vertices for grid calculations
1376
- vertex_0 = rectangle_vertices[0]
1377
- vertex_1 = rectangle_vertices[1]
1378
- vertex_3 = rectangle_vertices[3]
1379
-
1380
- # Calculate distances between vertices using geodetic calculations
1381
- dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1382
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1383
-
1384
- # Calculate side vectors in geographic coordinates
1385
- side_1 = np.array(vertex_1) - np.array(vertex_0)
1386
- side_2 = np.array(vertex_3) - np.array(vertex_0)
1387
-
1388
- # Create normalized unit vectors for grid orientation
1389
- u_vec = normalize_to_one_meter(side_1, dist_side_1)
1390
- v_vec = normalize_to_one_meter(side_2, dist_side_2)
1391
-
1392
- # Set grid origin and calculate optimal grid size
1393
- origin = np.array(rectangle_vertices[0])
1394
- grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1395
-
1396
- print(f"Calculated grid size: {grid_size}")
1397
- # print(f"Adjusted mesh size: {adjusted_meshsize}")
1398
-
1399
- # Set up coordinate transformation for basemap compatibility
1400
- geotiff_crs = CRS.from_epsg(3857)
1401
- transformer = setup_transformer(CRS.from_epsg(4326), geotiff_crs)
1402
-
1403
- # Generate grid cell coordinates (not currently used but available for advanced processing)
1404
- cell_coords = create_coordinate_mesh(origin, grid_size, adjusted_meshsize, u_vec, v_vec)
1405
- cell_coords_flat = cell_coords.reshape(2, -1).T
1406
- transformed_coords = np.array([transform_coords(transformer, lon, lat) for lat, lon in cell_coords_flat])
1407
- transformed_coords = transformed_coords.reshape(grid_size[::-1] + (2,))
1408
-
1409
- # print(f"Grid shape: {grid.shape}")
1410
-
1411
- # Create the visualization using the general plot_grid function
1412
- plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1413
- rectangle_vertices, 'land_cover', alpha=alpha, buf=buf, edge=edge, basemap=basemap, land_cover_classes=land_cover_classes)
1414
-
1415
- # Display information about the land cover classes present in the grid
1416
- unique_indices = np.unique(grid)
1417
- unique_classes = [list(land_cover_classes.values())[i] for i in unique_indices]
1418
- # print(f"Unique classes in the grid: {unique_classes}")
1419
-
1420
- def visualize_building_height_grid_on_map(building_height_grid, filtered_buildings, rectangle_vertices, meshsize, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1421
- # Calculate grid and normalize vectors
1422
- geod = initialize_geod()
1423
- vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
1424
-
1425
- dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1426
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1427
-
1428
- side_1 = np.array(vertex_1) - np.array(vertex_0)
1429
- side_2 = np.array(vertex_3) - np.array(vertex_0)
1430
-
1431
- u_vec = normalize_to_one_meter(side_1, dist_side_1)
1432
- v_vec = normalize_to_one_meter(side_2, dist_side_2)
1433
-
1434
- origin = np.array(rectangle_vertices[0])
1435
- _, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1436
-
1437
- # Setup transformer and plotting extent
1438
- transformer = setup_transformer(CRS.from_epsg(4326), CRS.from_epsg(3857))
1439
-
1440
- # Plot the results
1441
- plot_grid(building_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1442
- rectangle_vertices, 'building_height', vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap, buildings=filtered_buildings)
1443
-
1444
- def visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, meshsize, type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1445
- # Calculate grid and normalize vectors
1446
- geod = initialize_geod()
1447
- vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
1448
-
1449
- dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1450
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1451
-
1452
- side_1 = np.array(vertex_1) - np.array(vertex_0)
1453
- side_2 = np.array(vertex_3) - np.array(vertex_0)
1454
-
1455
- u_vec = normalize_to_one_meter(side_1, dist_side_1)
1456
- v_vec = normalize_to_one_meter(side_2, dist_side_2)
1457
-
1458
- origin = np.array(rectangle_vertices[0])
1459
- _, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1460
-
1461
- # Setup transformer and plotting extent
1462
- transformer = setup_transformer(CRS.from_epsg(4326), CRS.from_epsg(3857))
1463
-
1464
- # Plot the results
1465
- plot_grid(canopy_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1466
- rectangle_vertices, type, vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap)
1467
-
1468
- def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
1469
- all_classes = list(land_cover_classes.values())
1470
- unique_classes = list(dict.fromkeys(all_classes)) # Preserve order and remove duplicates
1471
-
1472
- colors = [color_map[cls] for cls in unique_classes]
1473
- cmap = mcolors.ListedColormap(colors)
1474
-
1475
- bounds = np.arange(len(unique_classes) + 1)
1476
- norm = mcolors.BoundaryNorm(bounds, cmap.N)
1477
-
1478
- class_to_num = {cls: i for i, cls in enumerate(unique_classes)}
1479
- numeric_grid = np.vectorize(class_to_num.get)(grid)
1480
-
1481
- plt.figure(figsize=(10, 10))
1482
- im = plt.imshow(numeric_grid, cmap=cmap, norm=norm, interpolation='nearest')
1483
- cbar = plt.colorbar(im, ticks=bounds[:-1] + 0.5)
1484
- cbar.set_ticklabels(unique_classes)
1485
- plt.title(f'Land Use/Land Cover Grid (Mesh Size: {mesh_size}m)')
1486
- plt.xlabel('Grid Cells (X)')
1487
- plt.ylabel('Grid Cells (Y)')
1488
- plt.show()
1489
-
1490
- def visualize_numerical_grid(grid, mesh_size, title, cmap='viridis', label='Value', vmin=None, vmax=None):
1491
- plt.figure(figsize=(10, 10))
1492
- plt.imshow(grid, cmap=cmap, vmin=vmin, vmax=vmax)
1493
- plt.colorbar(label=label)
1494
- plt.title(f'{title} (Mesh Size: {mesh_size}m)')
1495
- plt.xlabel('Grid Cells (X)')
1496
- plt.ylabel('Grid Cells (Y)')
1497
- plt.show()
1498
-
1499
- def convert_coordinates(coords):
1500
- return coords
1501
-
1502
- def calculate_centroid(coords):
1503
- lat_sum = sum(coord[0] for coord in coords)
1504
- lon_sum = sum(coord[1] for coord in coords)
1505
- return [lat_sum / len(coords), lon_sum / len(coords)]
1506
-
1507
- def calculate_center(features):
1508
- lats = []
1509
- lons = []
1510
- for feature in features:
1511
- coords = feature['geometry']['coordinates'][0]
1512
- for lat, lon in coords:
1513
- lats.append(lat)
1514
- lons.append(lon)
1515
- return sum(lats) / len(lats), sum(lons) / len(lons)
1516
-
1517
- def create_circle_polygon(center_lat, center_lon, radius_meters):
1518
- """Create a circular polygon with given center and radius"""
1519
- # Convert radius from meters to degrees (approximate)
1520
- radius_deg = radius_meters / 111000 # 1 degree ≈ 111km at equator
1521
-
1522
- # Create circle points
1523
- points = []
1524
- for angle in range(361): # 0 to 360 degrees
1525
- rad = math.radians(angle)
1526
- lat = center_lat + (radius_deg * math.cos(rad))
1527
- lon = center_lon + (radius_deg * math.sin(rad) / math.cos(math.radians(center_lat)))
1528
- points.append((lat, lon))
1529
- return Polygon(points)
1530
-
1531
- def visualize_landcover_grid_on_basemap(landcover_grid, rectangle_vertices, meshsize, source='Standard', alpha=0.6, figsize=(12, 8),
1532
- basemap='CartoDB light', show_edge=False, edge_color='black', edge_width=0.5):
1533
- """Visualizes a land cover grid GeoDataFrame using predefined color schemes.
1534
-
1535
- Args:
1536
- gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1537
- source: Source of land cover classification (e.g., 'Standard', 'Urbanwatch', etc.)
1538
- title: Title for the plot (default: None)
1539
- alpha: Transparency of the grid overlay (default: 0.6)
1540
- figsize: Figure size in inches (default: (12, 8))
1541
- basemap: Basemap style (default: 'CartoDB light')
1542
- show_edge: Whether to show cell edges (default: True)
1543
- edge_color: Color of cell edges (default: 'black')
1544
- edge_width: Width of cell edges (default: 0.5)
1545
- """
1546
- # Get land cover classes and colors
1547
- land_cover_classes = get_land_cover_classes(source)
1548
-
1549
- gdf = grid_to_geodataframe(landcover_grid, rectangle_vertices, meshsize)
1550
-
1551
- # Convert RGB tuples to normalized RGB values
1552
- colors = [(r/255, g/255, b/255) for (r,g,b) in land_cover_classes.keys()]
1553
-
1554
- # Create custom colormap
1555
- cmap = ListedColormap(colors)
1556
-
1557
- # Create bounds for discrete colorbar
1558
- bounds = np.arange(len(colors) + 1)
1559
- norm = BoundaryNorm(bounds, cmap.N)
1560
-
1561
- # Convert to Web Mercator
1562
- gdf_web = gdf.to_crs(epsg=3857)
1563
-
1564
- # Create figure and axis
1565
- fig, ax = plt.subplots(figsize=figsize)
1566
-
1567
- # Plot the GeoDataFrame
1568
- gdf_web.plot(column='value',
1569
- ax=ax,
1570
- alpha=alpha,
1571
- cmap=cmap,
1572
- norm=norm,
1573
- legend=True,
1574
- legend_kwds={
1575
- 'label': 'Land Cover Class',
1576
- 'ticks': bounds[:-1] + 0.5,
1577
- 'boundaries': bounds,
1578
- 'format': lambda x, p: list(land_cover_classes.values())[int(x)]
1579
- },
1580
- edgecolor=edge_color if show_edge else 'none',
1581
- linewidth=edge_width if show_edge else 0)
1582
-
1583
- # Add basemap
1584
- basemaps = {
1585
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1586
- 'CartoDB light': ctx.providers.CartoDB.Positron,
1587
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1588
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1589
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1590
- }
1591
- ctx.add_basemap(ax, source=basemaps[basemap])
1592
-
1593
- # Set title and remove axes
1594
- ax.set_axis_off()
1595
-
1596
- plt.tight_layout()
1597
- plt.show()
1598
-
1599
- def visualize_numerical_grid_on_basemap(grid, rectangle_vertices, meshsize, value_name="value", cmap='viridis', vmin=None, vmax=None,
1600
- alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1601
- show_edge=False, edge_color='black', edge_width=0.5):
1602
- """Visualizes a numerical grid GeoDataFrame (e.g., heights) on a basemap.
1603
-
1604
- Args:
1605
- gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1606
- title: Title for the plot (default: None)
1607
- cmap: Colormap to use (default: 'viridis')
1608
- vmin: Minimum value for colormap scaling (default: None)
1609
- vmax: Maximum value for colormap scaling (default: None)
1610
- alpha: Transparency of the grid overlay (default: 0.6)
1611
- figsize: Figure size in inches (default: (12, 8))
1612
- basemap: Basemap style (default: 'CartoDB light')
1613
- show_edge: Whether to show cell edges (default: True)
1614
- edge_color: Color of cell edges (default: 'black')
1615
- edge_width: Width of cell edges (default: 0.5)
1616
- """
1617
-
1618
- gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
1619
-
1620
- # Convert to Web Mercator
1621
- gdf_web = gdf.to_crs(epsg=3857)
1622
-
1623
- # Create figure and axis
1624
- fig, ax = plt.subplots(figsize=figsize)
1625
-
1626
- # Plot the GeoDataFrame
1627
- gdf_web.plot(column='value',
1628
- ax=ax,
1629
- alpha=alpha,
1630
- cmap=cmap,
1631
- vmin=vmin,
1632
- vmax=vmax,
1633
- legend=True,
1634
- legend_kwds={'label': value_name},
1635
- edgecolor=edge_color if show_edge else 'none',
1636
- linewidth=edge_width if show_edge else 0)
1637
-
1638
- # Add basemap
1639
- basemaps = {
1640
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1641
- 'CartoDB light': ctx.providers.CartoDB.Positron,
1642
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1643
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1644
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1645
- }
1646
- ctx.add_basemap(ax, source=basemaps[basemap])
1647
-
1648
- # Set title and remove axes
1649
- ax.set_axis_off()
1650
-
1651
- plt.tight_layout()
1652
- plt.show()
1653
-
1654
- def visualize_numerical_gdf_on_basemap(gdf, value_name="value", cmap='viridis', vmin=None, vmax=None,
1655
- alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1656
- show_edge=False, edge_color='black', edge_width=0.5):
1657
- """Visualizes a GeoDataFrame with numerical values on a basemap.
1658
-
1659
- Args:
1660
- gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1661
- value_name: Name of the value column and legend label (default: "value")
1662
- cmap: Colormap to use (default: 'viridis')
1663
- vmin: Minimum value for colormap scaling (default: None)
1664
- vmax: Maximum value for colormap scaling (default: None)
1665
- alpha: Transparency of the grid overlay (default: 0.6)
1666
- figsize: Figure size in inches (default: (12, 8))
1667
- basemap: Basemap style (default: 'CartoDB light')
1668
- show_edge: Whether to show cell edges (default: False)
1669
- edge_color: Color of cell edges (default: 'black')
1670
- edge_width: Width of cell edges (default: 0.5)
1671
- """
1672
- # Convert to Web Mercator if not already in that CRS
1673
- if gdf.crs != 'EPSG:3857':
1674
- gdf_web = gdf.to_crs(epsg=3857)
1675
- else:
1676
- gdf_web = gdf
1677
-
1678
- # Create figure and axis
1679
- fig, ax = plt.subplots(figsize=figsize)
1680
-
1681
- # Plot the GeoDataFrame
1682
- gdf_web.plot(column=value_name,
1683
- ax=ax,
1684
- alpha=alpha,
1685
- cmap=cmap,
1686
- vmin=vmin,
1687
- vmax=vmax,
1688
- legend=True,
1689
- legend_kwds={'label': value_name},
1690
- edgecolor=edge_color if show_edge else 'none',
1691
- linewidth=edge_width if show_edge else 0)
1692
-
1693
- # Add basemap
1694
- basemaps = {
1695
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1696
- 'CartoDB light': ctx.providers.CartoDB.Positron,
1697
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1698
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1699
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1700
- }
1701
- ctx.add_basemap(ax, source=basemaps[basemap])
1702
-
1703
- # Set title and remove axes
1704
- ax.set_axis_off()
1705
-
1706
- plt.tight_layout()
1707
- plt.show()
1708
-
1709
- def visualize_point_gdf_on_basemap(point_gdf, value_name='value', **kwargs):
1710
- """Visualizes a point GeoDataFrame on a basemap with colors based on values.
1711
-
1712
- Args:
1713
- point_gdf: GeoDataFrame with point geometries and values
1714
- value_name: Name of the column containing values to visualize (default: 'value')
1715
- **kwargs: Optional visualization parameters including:
1716
- - figsize: Tuple for figure size (default: (12, 8))
1717
- - colormap: Matplotlib colormap name (default: 'viridis')
1718
- - markersize: Size of points (default: 20)
1719
- - alpha: Transparency of points (default: 0.7)
1720
- - vmin: Minimum value for colormap scaling (default: None)
1721
- - vmax: Maximum value for colormap scaling (default: None)
1722
- - title: Plot title (default: None)
1723
- - basemap_style: Contextily basemap style (default: CartoDB.Positron)
1724
- - zoom: Basemap zoom level (default: 15)
1725
-
1726
- Returns:
1727
- matplotlib figure and axis objects
1728
- """
1729
- import matplotlib.pyplot as plt
1730
- import contextily as ctx
1731
-
1732
- # Set default parameters
1733
- defaults = {
1734
- 'figsize': (12, 8),
1735
- 'colormap': 'viridis',
1736
- 'markersize': 20,
1737
- 'alpha': 0.7,
1738
- 'vmin': None,
1739
- 'vmax': None,
1740
- 'title': None,
1741
- 'basemap_style': ctx.providers.CartoDB.Positron,
1742
- 'zoom': 15
1743
- }
1744
-
1745
- # Update defaults with provided kwargs
1746
- settings = {**defaults, **kwargs}
1747
-
1748
- # Create figure and axis
1749
- fig, ax = plt.subplots(figsize=settings['figsize'])
1750
-
1751
- # Convert to Web Mercator for basemap compatibility
1752
- point_gdf_web = point_gdf.to_crs(epsg=3857)
1753
-
1754
- # Plot points
1755
- scatter = point_gdf_web.plot(
1756
- column=value_name,
1757
- ax=ax,
1758
- cmap=settings['colormap'],
1759
- markersize=settings['markersize'],
1760
- alpha=settings['alpha'],
1761
- vmin=settings['vmin'],
1762
- vmax=settings['vmax'],
1763
- legend=True,
1764
- legend_kwds={
1765
- 'label': value_name,
1766
- 'orientation': 'vertical',
1767
- 'shrink': 0.8
1768
- }
1769
- )
1770
-
1771
- # Add basemap
1772
- ctx.add_basemap(
1773
- ax,
1774
- source=settings['basemap_style'],
1775
- zoom=settings['zoom']
1776
- )
1777
-
1778
- # Set title if provided
1779
- if settings['title']:
1780
- plt.title(settings['title'])
1781
-
1782
- # Remove axes
1783
- ax.set_axis_off()
1784
-
1785
- # Adjust layout to prevent colorbar cutoff
1786
- plt.tight_layout()
1787
- plt.show()
1788
-
1789
- def create_multi_view_scene(meshes, output_directory="output", projection_type="perspective", distance_factor=1.0):
1790
- """
1791
- Creates multiple rendered views of 3D city meshes from different camera angles.
1792
-
1793
- This function generates a comprehensive set of views including isometric and
1794
- orthographic projections of the 3D city model. Each view is rendered as a
1795
- high-quality image and saved to the specified directory.
1796
-
1797
- Parameters:
1798
- -----------
1799
- meshes : dict
1800
- Dictionary mapping mesh names/IDs to trimesh.Trimesh objects.
1801
- Each mesh represents a different component of the city model.
1802
-
1803
- output_directory : str, optional
1804
- Directory path where rendered images will be saved. Default is "output".
1805
-
1806
- projection_type : str, optional
1807
- Camera projection type. Options:
1808
- - "perspective": Natural perspective projection (default)
1809
- - "orthographic": Technical orthographic projection
1810
-
1811
- distance_factor : float, optional
1812
- Multiplier for camera distance from the scene. Default is 1.0.
1813
- Higher values move camera further away, lower values bring it closer.
1814
-
1815
- Returns:
1816
- --------
1817
- list of tuple
1818
- List of (view_name, filename) pairs for each generated image.
1819
-
1820
- Notes:
1821
- ------
1822
- - Generates 9 different views: 4 isometric + 5 orthographic
1823
- - Isometric views: front-right, front-left, back-right, back-left
1824
- - Orthographic views: top, front, back, left, right
1825
- - Uses PyVista for high-quality rendering with proper lighting
1826
- - Camera positions are automatically calculated based on scene bounds
1827
- - Images are saved as PNG files with high DPI
1828
-
1829
- Technical Details:
1830
- ------------------
1831
- - Scene bounding box is computed from all mesh vertices
1832
- - Camera distance is scaled based on scene diagonal
1833
- - Orthographic projection uses parallel scaling for technical drawings
1834
- - Each view uses optimized lighting for clarity
1835
-
1836
- Examples:
1837
- ---------
1838
- >>> meshes = {'buildings': building_mesh, 'ground': ground_mesh}
1839
- >>> views = create_multi_view_scene(meshes, "renders/", "orthographic", 1.5)
1840
- >>> print(f"Generated {len(views)} views")
1841
- """
1842
- # Compute overall bounding box across all meshes
1843
- vertices_list = [mesh.vertices for mesh in meshes.values()]
1844
- all_vertices = np.vstack(vertices_list)
1845
- bbox = np.array([
1846
- [all_vertices[:, 0].min(), all_vertices[:, 1].min(), all_vertices[:, 2].min()],
1847
- [all_vertices[:, 0].max(), all_vertices[:, 1].max(), all_vertices[:, 2].max()]
1848
- ])
1849
-
1850
- # Compute the center and diagonal of the bounding box
1851
- center = (bbox[1] + bbox[0]) / 2
1852
- diagonal = np.linalg.norm(bbox[1] - bbox[0])
1853
-
1854
- # Adjust distance based on projection type
1855
- if projection_type.lower() == "orthographic":
1856
- distance = diagonal * 5 # Increase distance for orthographic to capture full scene
1857
- else:
1858
- distance = diagonal * 1.8 * distance_factor # Original distance for perspective
1859
-
1860
- # Define the isometric viewing angles
1861
- iso_angles = {
1862
- 'iso_front_right': (1, 1, 0.7),
1863
- 'iso_front_left': (-1, 1, 0.7),
1864
- 'iso_back_right': (1, -1, 0.7),
1865
- 'iso_back_left': (-1, -1, 0.7)
1866
- }
1867
-
1868
- # Compute camera positions for isometric views
1869
- camera_positions = {}
1870
- for name, direction in iso_angles.items():
1871
- direction = np.array(direction)
1872
- direction = direction / np.linalg.norm(direction)
1873
- camera_pos = center + direction * distance
1874
- camera_positions[name] = [camera_pos, center, (0, 0, 1)]
1875
-
1876
- # Add orthographic views
1877
- ortho_views = {
1878
- 'xy_top': [center + np.array([0, 0, distance]), center, (-1, 0, 0)],
1879
- 'yz_right': [center + np.array([distance, 0, 0]), center, (0, 0, 1)],
1880
- 'xz_front': [center + np.array([0, distance, 0]), center, (0, 0, 1)],
1881
- 'yz_left': [center + np.array([-distance, 0, 0]), center, (0, 0, 1)],
1882
- 'xz_back': [center + np.array([0, -distance, 0]), center, (0, 0, 1)]
1883
- }
1884
- camera_positions.update(ortho_views)
1885
-
1886
- images = []
1887
- for view_name, camera_pos in camera_positions.items():
1888
- # Create new plotter for each view
1889
- plotter = pv.Plotter(notebook=True, off_screen=True)
1890
-
1891
- # Set the projection type
1892
- if projection_type.lower() == "orthographic":
1893
- plotter.enable_parallel_projection()
1894
- # Set parallel scale to ensure the whole scene is visible
1895
- plotter.camera.parallel_scale = diagonal * 0.4 * distance_factor # Adjust this factor as needed
1896
-
1897
- elif projection_type.lower() != "perspective":
1898
- print(f"Warning: Unknown projection_type '{projection_type}'. Using perspective projection.")
1899
-
1900
- # Add each mesh to the scene
1901
- for class_id, mesh in meshes.items():
1902
- vertices = mesh.vertices
1903
- faces = np.hstack([[3, *face] for face in mesh.faces])
1904
- pv_mesh = pv.PolyData(vertices, faces)
1905
-
1906
- if hasattr(mesh.visual, 'face_colors'):
1907
- colors = mesh.visual.face_colors
1908
- if colors.max() > 1:
1909
- colors = colors / 255.0
1910
- pv_mesh.cell_data['colors'] = colors
1911
-
1912
- plotter.add_mesh(pv_mesh,
1913
- rgb=True,
1914
- scalars='colors' if hasattr(mesh.visual, 'face_colors') else None)
1915
-
1916
- # Set camera position for this view
1917
- plotter.camera_position = camera_pos
1918
-
1919
- # Save screenshot
1920
- filename = f'{output_directory}/city_view_{view_name}.png'
1921
- plotter.screenshot(filename)
1922
- images.append((view_name, filename))
1923
- plotter.close()
1924
-
1925
- return images
1926
-
1927
- def visualize_voxcity_multi_view(voxel_array, meshsize, **kwargs):
1928
- """
1929
- Creates comprehensive 3D visualizations of voxel city data with multiple viewing angles.
1930
-
1931
- This is the primary function for generating publication-quality renderings of voxel
1932
- city models. It converts voxel data to 3D meshes, optionally overlays simulation
1933
- results, and produces multiple rendered views from different camera positions.
1934
-
1935
- Parameters:
1936
- -----------
1937
- voxel_array : numpy.ndarray
1938
- 3D array containing voxel class IDs. Shape should be (x, y, z).
1939
-
1940
- meshsize : float
1941
- Physical size of each voxel in meters.
1942
-
1943
- **kwargs : dict
1944
- Optional visualization parameters:
1945
-
1946
- Color and Style:
1947
- - voxel_color_map (str): Color scheme name, default 'default'
1948
- - output_directory (str): Directory for output files, default 'output'
1949
- - output_file_name (str): Base name for exported files
1950
-
1951
- Simulation Overlay:
1952
- - sim_grid (numpy.ndarray): 2D simulation results to overlay
1953
- - dem_grid (numpy.ndarray): Digital elevation model for height reference
1954
- - view_point_height (float): Height offset for simulation surface, default 1.5m
1955
- - colormap (str): Matplotlib colormap for simulation data, default 'viridis'
1956
- - vmin, vmax (float): Color scale limits for simulation data
1957
-
1958
- Camera and Rendering:
1959
- - projection_type (str): 'perspective' or 'orthographic', default 'perspective'
1960
- - distance_factor (float): Camera distance multiplier, default 1.0
1961
- - window_width, window_height (int): Render resolution, default 1024x768
1962
-
1963
- Output Control:
1964
- - show_views (bool): Whether to display rendered views, default True
1965
- - save_obj (bool): Whether to export OBJ mesh files, default False
1966
-
1967
- Returns:
1968
- --------
1969
- None
1970
- Displays rendered views and optionally saves files to disk.
1971
-
1972
- Notes:
1973
- ------
1974
- - Automatically configures PyVista for headless rendering
1975
- - Generates meshes for each voxel class with appropriate colors
1976
- - Creates colorbar for simulation data if provided
1977
- - Produces 9 different camera views (4 isometric + 5 orthographic)
1978
- - Exports mesh files in OBJ format if requested
1979
-
1980
- Technical Requirements:
1981
- -----------------------
1982
- - Requires Xvfb for headless rendering on Linux systems
1983
- - Uses PyVista for high-quality 3D rendering
1984
- - Simulation data is interpolated onto elevated surface mesh
1985
-
1986
- Examples:
1987
- ---------
1988
- >>> # Basic visualization
1989
- >>> visualize_voxcity_multi_view(voxel_array, meshsize=2.0)
1990
-
1991
- >>> # With simulation results overlay
1992
- >>> visualize_voxcity_multi_view(
1993
- ... voxel_array, 2.0,
1994
- ... sim_grid=temperature_data,
1995
- ... dem_grid=elevation_data,
1996
- ... colormap='plasma',
1997
- ... output_file_name='temperature_analysis'
1998
- ... )
1999
-
2000
- >>> # High-resolution orthographic technical drawings
2001
- >>> visualize_voxcity_multi_view(
2002
- ... voxel_array, 2.0,
2003
- ... projection_type='orthographic',
2004
- ... window_width=2048,
2005
- ... window_height=1536,
2006
- ... save_obj=True
2007
- ... )
2008
- """
2009
- # Set up headless rendering environment for PyVista
2010
- os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2011
- os.environ['DISPLAY'] = ':99'
2012
-
2013
- # Configure PyVista settings for high-quality rendering
2014
- pv.set_plot_theme('document')
2015
- pv.global_theme.background = 'white'
2016
- pv.global_theme.window_size = [1024, 768]
2017
- pv.global_theme.jupyter_backend = 'static'
2018
-
2019
- # Parse visualization parameters from kwargs
2020
- voxel_color_map = kwargs.get("voxel_color_map", 'default')
2021
- vox_dict = get_voxel_color_map(voxel_color_map)
2022
- output_directory = kwargs.get("output_directory", 'output')
2023
- base_filename = kwargs.get("output_file_name", None)
2024
- sim_grid = kwargs.get("sim_grid", None)
2025
- dem_grid_ori = kwargs.get("dem_grid", None)
2026
-
2027
- # Normalize DEM grid to start from zero elevation
2028
- if dem_grid_ori is not None:
2029
- dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2030
-
2031
- # Simulation overlay parameters
2032
- z_offset = kwargs.get("view_point_height", 1.5)
2033
- cmap_name = kwargs.get("colormap", "viridis")
2034
- vmin = kwargs.get("vmin", np.nanmin(sim_grid) if sim_grid is not None else None)
2035
- vmax = kwargs.get("vmax", np.nanmax(sim_grid) if sim_grid is not None else None)
2036
-
2037
- # Camera and rendering parameters
2038
- projection_type = kwargs.get("projection_type", "perspective")
2039
- distance_factor = kwargs.get("distance_factor", 1.0)
2040
-
2041
- # Output control parameters
2042
- save_obj = kwargs.get("save_obj", False)
2043
- show_views = kwargs.get("show_views", True)
2044
-
2045
- # Create 3D meshes from voxel data
2046
- print("Creating voxel meshes...")
2047
- meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2048
-
2049
- # Add simulation results as elevated surface mesh if provided
2050
- if sim_grid is not None and dem_grid is not None:
2051
- print("Creating sim_grid surface mesh...")
2052
- sim_mesh = create_sim_surface_mesh(
2053
- sim_grid, dem_grid,
2054
- meshsize=meshsize,
2055
- z_offset=z_offset,
2056
- cmap_name=cmap_name,
2057
- vmin=vmin,
2058
- vmax=vmax
2059
- )
2060
- if sim_mesh is not None:
2061
- meshes["sim_surface"] = sim_mesh
2062
-
2063
- # Create and display colorbar for simulation data
2064
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2065
- scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2066
-
2067
- fig, ax = plt.subplots(figsize=(6, 1))
2068
- plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2069
- plt.tight_layout()
2070
- plt.show()
2071
-
2072
- # Export mesh files if requested
2073
- if base_filename is not None:
2074
- print(f"Exporting files to '{base_filename}.*' ...")
2075
- os.makedirs(output_directory, exist_ok=True)
2076
- export_meshes(meshes, output_directory, base_filename)
2077
-
2078
- # Generate and display multiple camera views
2079
- if show_views:
2080
- print("Creating multiple views...")
2081
- os.makedirs(output_directory, exist_ok=True)
2082
- image_files = create_multi_view_scene(meshes, output_directory=output_directory, projection_type=projection_type, distance_factor=distance_factor)
2083
-
2084
- # Display each rendered view
2085
- for view_name, img_file in image_files:
2086
- plt.figure(figsize=(24, 16))
2087
- img = plt.imread(img_file)
2088
- plt.imshow(img)
2089
- plt.title(view_name.replace('_', ' ').title(), pad=20)
2090
- plt.axis('off')
2091
- plt.show()
2092
- plt.close()
2093
-
2094
- # Export OBJ mesh files if requested
2095
- if save_obj:
2096
- output_directory = kwargs.get('output_directory', 'output')
2097
- output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2098
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2099
- print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2100
-
2101
- def visualize_voxcity_multi_view_with_multiple_sim_grids(voxel_array, meshsize, sim_configs, **kwargs):
2102
- """
2103
- Create multiple views of the voxel city data with multiple simulation grids.
2104
-
2105
- Args:
2106
- voxel_array: 3D numpy array containing voxel data
2107
- meshsize: Size of each voxel/cell
2108
- sim_configs: List of dictionaries, each containing configuration for a simulation grid:
2109
- {
2110
- 'sim_grid': 2D numpy array of simulation values,
2111
- 'z_offset': height offset in meters (default: 1.5),
2112
- 'cmap_name': colormap name (default: 'viridis'),
2113
- 'vmin': minimum value for colormap (optional),
2114
- 'vmax': maximum value for colormap (optional),
2115
- 'label': label for the colorbar (optional)
2116
- }
2117
- **kwargs: Additional arguments including:
2118
- - vox_dict: Dictionary mapping voxel values to colors
2119
- - output_directory: Directory to save output images
2120
- - output_file_name: Base filename for exports
2121
- - dem_grid: DEM grid for height information
2122
- - projection_type: 'perspective' or 'orthographic'
2123
- - distance_factor: Factor to adjust camera distance
2124
- """
2125
- os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2126
- os.environ['DISPLAY'] = ':99'
2127
-
2128
- # Configure PyVista settings
2129
- pv.set_plot_theme('document')
2130
- pv.global_theme.background = 'white'
2131
- window_width = kwargs.get("window_width", 1024)
2132
- window_height = kwargs.get("window_height", 768)
2133
- pv.global_theme.window_size = [window_width, window_height]
2134
- pv.global_theme.jupyter_backend = 'static'
2135
-
2136
- # Parse general kwargs
2137
- voxel_color_map = kwargs.get("voxel_color_map", 'default')
2138
- vox_dict = get_voxel_color_map(voxel_color_map)
2139
- output_directory = kwargs.get("output_directory", 'output')
2140
- base_filename = kwargs.get("output_file_name", None)
2141
- dem_grid_ori = kwargs.get("dem_grid", None)
2142
- projection_type = kwargs.get("projection_type", "perspective")
2143
- distance_factor = kwargs.get("distance_factor", 1.0)
2144
- show_views = kwargs.get("show_views", True)
2145
- save_obj = kwargs.get("save_obj", False)
2146
-
2147
- if dem_grid_ori is not None:
2148
- dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2149
-
2150
- # Create meshes
2151
- print("Creating voxel meshes...")
2152
- meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2153
-
2154
- # Process each simulation grid
2155
- for i, config in enumerate(sim_configs):
2156
- sim_grid = config['sim_grid']
2157
- if sim_grid is None or dem_grid is None:
2158
- continue
2159
-
2160
- z_offset = config.get('z_offset', 1.5)
2161
- cmap_name = config.get('cmap_name', 'viridis')
2162
- vmin = config.get('vmin', np.nanmin(sim_grid))
2163
- vmax = config.get('vmax', np.nanmax(sim_grid))
2164
- label = config.get('label', f'Simulation {i+1}')
2165
-
2166
- print(f"Creating sim_grid surface mesh for {label}...")
2167
- sim_mesh = create_sim_surface_mesh(
2168
- sim_grid, dem_grid,
2169
- meshsize=meshsize,
2170
- z_offset=z_offset,
2171
- cmap_name=cmap_name,
2172
- vmin=vmin,
2173
- vmax=vmax
2174
- )
2175
-
2176
- if sim_mesh is not None:
2177
- meshes[f"sim_surface_{i}"] = sim_mesh
2178
-
2179
- # Create colorbar for this simulation
2180
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2181
- scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2182
-
2183
- fig, ax = plt.subplots(figsize=(6, 1))
2184
- plt.colorbar(scalar_map, cax=ax, orientation='horizontal', label=label)
2185
- plt.tight_layout()
2186
- plt.show()
2187
-
2188
- # Export if filename provided
2189
- if base_filename is not None:
2190
- print(f"Exporting files to '{base_filename}.*' ...")
2191
- os.makedirs(output_directory, exist_ok=True)
2192
- export_meshes(meshes, output_directory, base_filename)
2193
-
2194
- if show_views:
2195
- # Create and save multiple views
2196
- print("Creating multiple views...")
2197
- os.makedirs(output_directory, exist_ok=True)
2198
- image_files = create_multi_view_scene(
2199
- meshes,
2200
- output_directory=output_directory,
2201
- projection_type=projection_type,
2202
- distance_factor=distance_factor
2203
- )
2204
-
2205
- # Display each view separately
2206
- for view_name, img_file in image_files:
2207
- plt.figure(figsize=(24, 16))
2208
- img = plt.imread(img_file)
2209
- plt.imshow(img)
2210
- plt.title(view_name.replace('_', ' ').title(), pad=20)
2211
- plt.axis('off')
2212
- plt.show()
2213
- plt.close()
2214
-
2215
- # After creating the meshes and before visualization
2216
- if save_obj:
2217
- output_directory = kwargs.get('output_directory', 'output')
2218
- output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2219
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2220
- print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2221
-
2222
- def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None, **kwargs):
2223
- """
2224
- Creates 3D visualizations of voxel city data with custom simulation mesh overlays.
2225
-
2226
- This advanced visualization function allows replacement of specific voxel classes
2227
- with custom simulation result meshes. It's particularly useful for overlaying
2228
- detailed simulation results (like computational fluid dynamics, thermal analysis,
2229
- or environmental factors) onto specific building or infrastructure components.
2230
-
2231
- The function supports simulation meshes with metadata containing numerical values
2232
- that can be visualized using color mapping, making it ideal for displaying
2233
- spatially-varying simulation results on building surfaces or other urban elements.
2234
-
2235
- Parameters:
2236
- -----------
2237
- voxel_array : np.ndarray
2238
- 3D array of voxel values representing the base city model.
2239
- Shape should be (x, y, z) where each element is a voxel class ID.
2240
-
2241
- meshsize : float
2242
- Size of each voxel in meters, used for coordinate scaling.
2243
-
2244
- custom_meshes : dict, optional
2245
- Dictionary mapping voxel class IDs to custom trimesh.Trimesh objects.
2246
- Example: {-3: building_simulation_mesh, -2: vegetation_mesh}
2247
- These meshes will replace the standard voxel representation for visualization.
2248
- Default is None.
2249
-
2250
- **kwargs:
2251
- Extensive configuration options organized by category:
2252
-
2253
- Base Visualization:
2254
- - vox_dict (dict): Dictionary mapping voxel class IDs to colors
2255
- - output_directory (str): Directory for saving output files
2256
- - output_file_name (str): Base filename for exported meshes
2257
-
2258
- Simulation Result Display:
2259
- - value_name (str): Name of metadata field containing simulation values
2260
- - colormap (str): Matplotlib colormap name for simulation results
2261
- - vmin, vmax (float): Color scale limits for simulation data
2262
- - colorbar_title (str): Title for the simulation result colorbar
2263
- - nan_color (str/tuple): Color for NaN/invalid simulation values, default 'gray'
2264
-
2265
- Ground Surface Overlay:
2266
- - sim_grid (np.ndarray): 2D array with ground-level simulation values
2267
- - dem_grid (np.ndarray): Digital elevation model for terrain height
2268
- - view_point_height (float): Height offset for ground simulation surface
2269
-
2270
- Camera and Rendering:
2271
- - projection_type (str): 'perspective' or 'orthographic', default 'perspective'
2272
- - distance_factor (float): Camera distance multiplier, default 1.0
2273
- - window_width, window_height (int): Render resolution
2274
-
2275
- Output Control:
2276
- - show_views (bool): Whether to display rendered views, default True
2277
- - save_obj (bool): Whether to export OBJ mesh files, default False
2278
-
2279
- Returns:
2280
- --------
2281
- list
2282
- List of (view_name, image_file_path) tuples for generated views.
2283
- Only returned if show_views=True.
2284
-
2285
- Notes:
2286
- ------
2287
- Simulation Mesh Requirements:
2288
- - Custom meshes should have simulation values stored in mesh.metadata[value_name]
2289
- - Values can include NaN for areas without valid simulation data
2290
- - Mesh geometry should align with the voxel grid coordinate system
2291
-
2292
- Color Mapping:
2293
- - Simulation values are mapped to colors using the specified colormap
2294
- - NaN values are rendered in the specified nan_color
2295
- - A colorbar is automatically generated and displayed
2296
-
2297
- Technical Implementation:
2298
- - Uses PyVista for high-quality 3D rendering
2299
- - Supports both individual mesh coloring and ground surface overlays
2300
- - Automatically handles coordinate system transformations
2301
- - Generates multiple camera views for comprehensive visualization
2302
-
2303
- Examples:
2304
- ---------
2305
- >>> # Basic usage with building simulation results
2306
- >>> building_mesh = trimesh.load('building_with_cfd_results.ply')
2307
- >>> building_mesh.metadata = {'temperature': temperature_values}
2308
- >>> custom_meshes = {-3: building_mesh} # -3 is building class ID
2309
- >>>
2310
- >>> visualize_voxcity_with_sim_meshes(
2311
- ... voxel_array, meshsize=2.0,
2312
- ... custom_meshes=custom_meshes,
2313
- ... value_name='temperature',
2314
- ... colormap='plasma',
2315
- ... colorbar_title='Temperature (°C)',
2316
- ... vmin=15, vmax=35
2317
- ... )
2318
-
2319
- >>> # With ground-level wind simulation overlay
2320
- >>> wind_mesh = create_wind_simulation_mesh(wind_data)
2321
- >>> visualize_voxcity_with_sim_meshes(
2322
- ... voxel_array, 2.0,
2323
- ... custom_meshes={-3: wind_mesh},
2324
- ... value_name='wind_speed',
2325
- ... sim_grid=ground_wind_grid,
2326
- ... dem_grid=elevation_grid,
2327
- ... colormap='viridis',
2328
- ... projection_type='orthographic',
2329
- ... save_obj=True
2330
- ... )
2331
-
2332
- >>> # Multiple simulation types with custom styling
2333
- >>> meshes = {
2334
- ... -3: building_thermal_mesh, # Buildings with thermal data
2335
- ... -2: vegetation_co2_mesh # Vegetation with CO2 absorption
2336
- ... }
2337
- >>> visualize_voxcity_with_sim_meshes(
2338
- ... voxel_array, 2.0,
2339
- ... custom_meshes=meshes,
2340
- ... value_name='co2_flux',
2341
- ... colormap='RdYlBu_r',
2342
- ... nan_color='lightgray',
2343
- ... distance_factor=1.5,
2344
- ... output_file_name='co2_analysis'
2345
- ... )
2346
-
2347
- See Also:
2348
- ---------
2349
- - visualize_building_sim_results(): Specialized function for building simulations
2350
- - visualize_voxcity_multi_view(): Basic voxel visualization without custom meshes
2351
- - create_multi_view_scene(): Lower-level rendering function
2352
- """
2353
- os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2354
- os.environ['DISPLAY'] = ':99'
2355
-
2356
- # Configure PyVista settings
2357
- pv.set_plot_theme('document')
2358
- pv.global_theme.background = 'white'
2359
- window_width = kwargs.get("window_width", 1024)
2360
- window_height = kwargs.get("window_height", 768)
2361
- pv.global_theme.window_size = [window_width, window_height]
2362
- pv.global_theme.jupyter_backend = 'static'
2363
-
2364
- # Parse kwargs
2365
- voxel_color_map = kwargs.get("voxel_color_map", 'default')
2366
- vox_dict = get_voxel_color_map(voxel_color_map)
2367
- output_directory = kwargs.get("output_directory", 'output')
2368
- base_filename = kwargs.get("output_file_name", None)
2369
- sim_grid = kwargs.get("sim_grid", None)
2370
- dem_grid_ori = kwargs.get("dem_grid", None)
2371
- if dem_grid_ori is not None:
2372
- dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2373
- z_offset = kwargs.get("view_point_height", 1.5)
2374
- cmap_name = kwargs.get("colormap", "viridis")
2375
- vmin = kwargs.get("vmin", None)
2376
- vmax = kwargs.get("vmax", None)
2377
- projection_type = kwargs.get("projection_type", "perspective")
2378
- distance_factor = kwargs.get("distance_factor", 1.0)
2379
- colorbar_title = kwargs.get("colorbar_title", "")
2380
- value_name = kwargs.get("value_name", None)
2381
- nan_color = kwargs.get("nan_color", "gray")
2382
- show_views = kwargs.get("show_views", True)
2383
- save_obj = kwargs.get("save_obj", False)
2384
-
2385
- if value_name is None:
2386
- print("Set value_name")
2387
-
2388
- # Create meshes from voxel data
2389
- print("Creating voxel meshes...")
2390
- meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2391
-
2392
- # Replace specific voxel class meshes with custom simulation meshes
2393
- if custom_meshes is not None:
2394
- for class_id, custom_mesh in custom_meshes.items():
2395
- # Apply coloring to custom meshes if they have metadata values
2396
- if hasattr(custom_mesh, 'metadata') and value_name in custom_mesh.metadata:
2397
- # Create a colored copy of the mesh for visualization
2398
- import matplotlib.cm as cm
2399
- import matplotlib.colors as mcolors
2400
-
2401
- # Get values from metadata
2402
- values = custom_mesh.metadata[value_name]
2403
-
2404
- # Set vmin/vmax if not provided
2405
- local_vmin = vmin if vmin is not None else np.nanmin(values[~np.isnan(values)])
2406
- local_vmax = vmax if vmax is not None else np.nanmax(values[~np.isnan(values)])
2407
-
2408
- # Create colors
2409
- cmap = cm.get_cmap(cmap_name)
2410
- norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
2411
-
2412
- # Handle NaN values with custom color
2413
- face_colors = np.zeros((len(values), 4))
2414
-
2415
- # Convert string color to RGBA if needed
2416
- if isinstance(nan_color, str):
2417
- import matplotlib.colors as mcolors
2418
- nan_rgba = np.array(mcolors.to_rgba(nan_color))
2419
- else:
2420
- # Assume it's already a tuple/list of RGBA values
2421
- nan_rgba = np.array(nan_color)
2422
-
2423
- # Apply colors: NaN values get nan_color, others get colormap colors
2424
- nan_mask = np.isnan(values)
2425
- face_colors[~nan_mask] = cmap(norm(values[~nan_mask]))
2426
- face_colors[nan_mask] = nan_rgba
2427
-
2428
- # Create a copy with colors
2429
- vis_mesh = custom_mesh.copy()
2430
- vis_mesh.visual.face_colors = face_colors
2431
-
2432
- # Prepare the colormap and create colorbar
2433
- norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
2434
- scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2435
-
2436
- # Create a figure and axis for the colorbar but don't display
2437
- fig, ax = plt.subplots(figsize=(6, 1))
2438
- cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2439
- if colorbar_title:
2440
- cbar.set_label(colorbar_title)
2441
- plt.tight_layout()
2442
- plt.show()
2443
-
2444
- if class_id in meshes:
2445
- print(f"Replacing voxel class {class_id} with colored custom simulation mesh")
2446
- meshes[class_id] = vis_mesh
2447
- else:
2448
- print(f"Adding colored custom simulation mesh for class {class_id}")
2449
- meshes[class_id] = vis_mesh
2450
- else:
2451
- # No metadata values, use the mesh as is
2452
- if class_id in meshes:
2453
- print(f"Replacing voxel class {class_id} with custom simulation mesh")
2454
- meshes[class_id] = custom_mesh
2455
- else:
2456
- print(f"Adding custom simulation mesh for class {class_id}")
2457
- meshes[class_id] = custom_mesh
2458
-
2459
- # Create sim_grid surface mesh if provided
2460
- if sim_grid is not None and dem_grid is not None:
2461
- print("Creating sim_grid surface mesh...")
2462
-
2463
- # If vmin/vmax not provided, use actual min/max of the valid sim data
2464
- if vmin is None:
2465
- vmin = np.nanmin(sim_grid)
2466
- if vmax is None:
2467
- vmax = np.nanmax(sim_grid)
2468
-
2469
- sim_mesh = create_sim_surface_mesh(
2470
- sim_grid, dem_grid,
2471
- meshsize=meshsize,
2472
- z_offset=z_offset,
2473
- cmap_name=cmap_name,
2474
- vmin=vmin,
2475
- vmax=vmax,
2476
- nan_color=nan_color # Pass nan_color to the mesh creation
2477
- )
2478
- if sim_mesh is not None:
2479
- meshes["sim_surface"] = sim_mesh
2480
-
2481
- # # Prepare the colormap and create colorbar
2482
- # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2483
- # scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2484
-
2485
- # # Create a figure and axis for the colorbar but don't display
2486
- # fig, ax = plt.subplots(figsize=(6, 1))
2487
- # cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2488
- # if colorbar_title:
2489
- # cbar.set_label(colorbar_title)
2490
- # plt.tight_layout()
2491
- # plt.show()
2492
-
2493
- # Export if filename provided
2494
- if base_filename is not None:
2495
- print(f"Exporting files to '{base_filename}.*' ...")
2496
- # Create output directory if it doesn't exist
2497
- os.makedirs(output_directory, exist_ok=True)
2498
- export_meshes(meshes, output_directory, base_filename)
2499
-
2500
-
2501
- # Create and save multiple views
2502
- print("Creating multiple views...")
2503
- # Create output directory if it doesn't exist
2504
- os.makedirs(output_directory, exist_ok=True)
2505
-
2506
- if show_views:
2507
- image_files = create_multi_view_scene(meshes, output_directory=output_directory,
2508
- projection_type=projection_type,
2509
- distance_factor=distance_factor)
2510
-
2511
- # Display each view separately
2512
- for view_name, img_file in image_files:
2513
- plt.figure(figsize=(24, 16))
2514
- img = plt.imread(img_file)
2515
- plt.imshow(img)
2516
- plt.title(view_name.replace('_', ' ').title(), pad=20)
2517
- plt.axis('off')
2518
- plt.show()
2519
- plt.close()
2520
-
2521
- # After creating the meshes and before visualization
2522
- if save_obj:
2523
- output_directory = kwargs.get('output_directory', 'output')
2524
- output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2525
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2526
- print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2527
-
2528
- def visualize_building_sim_results(voxel_array, meshsize, building_sim_mesh, **kwargs):
2529
- """
2530
- Visualize building simulation results by replacing building meshes in the original model.
2531
-
2532
- This is a specialized wrapper around visualize_voxcity_with_sim_meshes that specifically
2533
- targets building simulation meshes (assuming building class ID is -3).
2534
-
2535
- Parameters
2536
- ----------
2537
- voxel_array : np.ndarray
2538
- 3D array of voxel values.
2539
- meshsize : float
2540
- Size of each voxel in meters.
2541
- building_sim_mesh : trimesh.Trimesh
2542
- Simulation result mesh for buildings with values stored in metadata.
2543
- **kwargs:
2544
- Same parameters as visualize_voxcity_with_sim_meshes.
2545
- Additional parameters:
2546
- value_name : str
2547
- Name of the field in metadata containing values to visualize (default: 'svf_values')
2548
- nan_color : str or tuple
2549
- Color for NaN values (default: 'gray')
2550
-
2551
- Returns
2552
- -------
2553
- list
2554
- List of (view_name, image_file_path) tuples for the generated views.
2555
- """
2556
- # Building class ID is typically -3 in voxcity
2557
- building_class_id = kwargs.get("building_class_id", -3)
2558
-
2559
- # Create custom meshes dictionary with the building simulation mesh
2560
- custom_meshes = {building_class_id: building_sim_mesh}
2561
-
2562
- # Add colorbar title if not provided
2563
- if "colorbar_title" not in kwargs:
2564
- # Try to guess a title based on the mesh name/type
2565
- if hasattr(building_sim_mesh, 'name') and building_sim_mesh.name:
2566
- kwargs["colorbar_title"] = building_sim_mesh.name
2567
- else:
2568
- # Use value_field name as fallback
2569
- value_name = kwargs.get("value_name", "svf_values")
2570
- pretty_name = value_name.replace('_', ' ').title()
2571
- kwargs["colorbar_title"] = pretty_name
2572
-
2573
- # Call the more general visualization function
2574
- visualize_voxcity_with_sim_meshes(
2575
- voxel_array,
2576
- meshsize,
2577
- custom_meshes=custom_meshes,
2578
- **kwargs
1
+ """
2
+ VoxelCity Visualization Utilities
3
+
4
+ This module provides comprehensive visualization tools for 3D voxel city data,
5
+ including support for multiple color schemes, 3D plotting with matplotlib and plotly,
6
+ grid visualization on basemaps, and mesh-based rendering with PyVista.
7
+
8
+ The module handles various data types including:
9
+ - Land cover classifications
10
+ - Building heights and footprints
11
+ - Digital elevation models (DEM)
12
+ - Canopy heights
13
+ - View indices (sky view factor, green view index)
14
+ - Simulation results on building surfaces
15
+
16
+ Key Features:
17
+ - Multiple predefined color schemes for voxel visualization
18
+ - 2D and 3D plotting capabilities
19
+ - Interactive web maps with folium
20
+ - Mesh export functionality (OBJ format)
21
+ - Multi-view scene generation
22
+ - Custom simulation result overlays
23
+ """
24
+
25
+ import numpy as np
26
+ import matplotlib.pyplot as plt
27
+ from mpl_toolkits.mplot3d import Axes3D
28
+ from tqdm import tqdm
29
+ import matplotlib.colors as mcolors
30
+ from matplotlib.colors import ListedColormap, BoundaryNorm
31
+ import matplotlib.cm as cm
32
+ import contextily as ctx
33
+ from shapely.geometry import Polygon
34
+ import plotly.graph_objects as go
35
+ from tqdm import tqdm
36
+ import pyproj
37
+ # import rasterio
38
+ from pyproj import CRS
39
+ # from shapely.geometry import box
40
+ import seaborn as sns
41
+ import random
42
+ import folium
43
+ import math
44
+ import trimesh
45
+ import pyvista as pv
46
+ from IPython.display import display
47
+ import os
48
+
49
+ # Import utility functions for land cover classification
50
+ from .lc import get_land_cover_classes
51
+ # from ..geo.geojson import filter_buildings
52
+
53
+ # Import grid processing functions
54
+ from ..geoprocessor.grid import (
55
+ calculate_grid_size,
56
+ create_coordinate_mesh,
57
+ create_cell_polygon,
58
+ grid_to_geodataframe
59
+ )
60
+
61
+ # Import geospatial utility functions
62
+ from ..geoprocessor.utils import (
63
+ initialize_geod,
64
+ calculate_distance,
65
+ normalize_to_one_meter,
66
+ setup_transformer,
67
+ transform_coords,
68
+ )
69
+
70
+ # Import mesh generation and export functions
71
+ from ..geoprocessor.mesh import (
72
+ create_voxel_mesh,
73
+ create_sim_surface_mesh,
74
+ create_city_meshes,
75
+ export_meshes,
76
+ save_obj_from_colored_mesh
77
+ )
78
+ # from ..exporter.obj import save_obj_from_colored_mesh
79
+
80
+ # Import material property functions
81
+ from .material import get_material_dict
82
+
83
+ def get_voxel_color_map(color_scheme='default'):
84
+ """
85
+ Returns a color map for voxel visualization based on the specified color scheme.
86
+
87
+ This function provides multiple predefined color schemes for visualizing voxel data.
88
+ Each scheme maps voxel class IDs to RGB color values [0-255]. The class IDs follow
89
+ a specific convention where negative values represent built environment elements
90
+ and positive values represent natural/ground surface elements.
91
+
92
+ Voxel Class ID Convention:
93
+ -99: Void/empty space (black)
94
+ -30: Landmark buildings (special highlighting)
95
+ -17 to -11: Building materials (plaster, glass, stone, metal, concrete, wood, brick)
96
+ -3: Generic building structures
97
+ -2: Trees/vegetation (above ground)
98
+ -1: Underground/subsurface
99
+ 1-14: Ground surface land cover types (bareland, vegetation, water, etc.)
100
+
101
+ Parameters:
102
+ -----------
103
+ color_scheme : str, optional
104
+ The name of the color scheme to use. Available options:
105
+
106
+ Basic Schemes:
107
+ - 'default': Original balanced color scheme for general use
108
+ - 'high_contrast': High contrast colors for better visibility
109
+ - 'monochrome': Shades of blue for academic presentations
110
+ - 'pastel': Softer, muted colors for aesthetic appeal
111
+ - 'dark_mode': Darker colors for dark backgrounds
112
+ - 'grayscale': Black and white gradient with color accents
113
+
114
+ Thematic Schemes:
115
+ - 'autumn': Warm reds, oranges, and browns
116
+ - 'cool': Cool blues, purples, and cyans
117
+ - 'earth_tones': Natural earth colors
118
+ - 'vibrant': Very bright, saturated colors
119
+
120
+ Stylistic Schemes:
121
+ - 'cyberpunk': Neon-like purples, pinks, and blues
122
+ - 'tropical': Vibrant greens, oranges, pinks (island vibes)
123
+ - 'vintage': Muted, sepia-like tones
124
+ - 'neon_dreams': Super-bright, nightclub neon palette
125
+
126
+ Returns:
127
+ --------
128
+ dict
129
+ A dictionary mapping voxel class IDs (int) to RGB color values (list of 3 ints [0-255])
130
+
131
+ Examples:
132
+ ---------
133
+ >>> colors = get_voxel_color_map('default')
134
+ >>> print(colors[-3]) # Building color
135
+ [180, 187, 216]
136
+
137
+ >>> colors = get_voxel_color_map('cyberpunk')
138
+ >>> print(colors[9]) # Water color in cyberpunk scheme
139
+ [51, 0, 102]
140
+
141
+ Notes:
142
+ ------
143
+ - All color values are in RGB format with range [0, 255]
144
+ - The 'default' scheme should not be modified to maintain consistency
145
+ - Unknown color schemes will fall back to 'default' with a warning
146
+ - Color schemes can be extended by adding new elif blocks
147
+ """
148
+ # ----------------------
149
+ # DO NOT MODIFY DEFAULT
150
+ # ----------------------
151
+ if color_scheme == 'default':
152
+ return {
153
+ -99: [0, 0, 0], # void,
154
+ -30: [255, 0, 102], # (Pink) 'Landmark',
155
+ -17: [238, 242, 234], # (light gray) 'plaster',
156
+ -16: [56, 78, 84], # (Dark blue) 'glass',
157
+ -15: [147, 140, 114], # (Light brown) 'stone',
158
+ -14: [139, 149, 159], # (Gray) 'metal',
159
+ -13: [186, 187, 181], # (Gray) 'concrete',
160
+ -12: [248, 166, 2], # (Orange) 'wood',
161
+ -11: [81, 59, 56], # (Dark red) 'brick',
162
+ -3: [180, 187, 216], # Building
163
+ -2: [78, 99, 63], # Tree
164
+ -1: [188, 143, 143], # Underground
165
+ 1: [239, 228, 176], # 'Bareland (ground surface)',
166
+ 2: [123, 130, 59], # 'Rangeland (ground surface)',
167
+ 3: [97, 140, 86], # 'Shrub (ground surface)',
168
+ 4: [112, 120, 56], # 'Agriculture land (ground surface)',
169
+ 5: [116, 150, 66], # 'Tree (ground surface)',
170
+ 6: [187, 204, 40], # 'Moss and lichen (ground surface)',
171
+ 7: [77, 118, 99], # 'Wet land (ground surface)',
172
+ 8: [22, 61, 51], # 'Mangrove (ground surface)',
173
+ 9: [44, 66, 133], # 'Water (ground surface)',
174
+ 10: [205, 215, 224], # 'Snow and ice (ground surface)',
175
+ 11: [108, 119, 129], # 'Developed space (ground surface)',
176
+ 12: [59, 62, 87], # 'Road (ground surface)',
177
+ 13: [150, 166, 190], # 'Building (ground surface)'
178
+ 14: [239, 228, 176], # 'No Data (ground surface)'
179
+ }
180
+
181
+ elif color_scheme == 'high_contrast':
182
+ return {
183
+ -99: [0, 0, 0], # void
184
+ -30: [255, 0, 255], # (Bright Magenta) 'Landmark'
185
+ -17: [255, 255, 255], # (Pure White) 'plaster'
186
+ -16: [0, 0, 255], # (Bright Blue) 'glass'
187
+ -15: [153, 76, 0], # (Dark Brown) 'stone'
188
+ -14: [192, 192, 192], # (Silver) 'metal'
189
+ -13: [128, 128, 128], # (Gray) 'concrete'
190
+ -12: [255, 128, 0], # (Bright Orange) 'wood'
191
+ -11: [153, 0, 0], # (Dark Red) 'brick'
192
+ -3: [0, 255, 255], # (Cyan) Building
193
+ -2: [0, 153, 0], # (Green) Tree
194
+ -1: [204, 0, 102], # (Dark Pink) Underground
195
+ 1: [255, 255, 153], # (Light Yellow) 'Bareland'
196
+ 2: [102, 153, 0], # (Olive Green) 'Rangeland'
197
+ 3: [0, 204, 0], # (Bright Green) 'Shrub'
198
+ 4: [153, 204, 0], # (Yellowish Green) 'Agriculture land'
199
+ 5: [0, 102, 0], # (Dark Green) 'Tree'
200
+ 6: [204, 255, 51], # (Lime Green) 'Moss and lichen'
201
+ 7: [0, 153, 153], # (Teal) 'Wet land'
202
+ 8: [0, 51, 0], # (Very Dark Green) 'Mangrove'
203
+ 9: [0, 102, 204], # (Bright Blue) 'Water'
204
+ 10: [255, 255, 255], # (White) 'Snow and ice'
205
+ 11: [76, 76, 76], # (Dark Gray) 'Developed space'
206
+ 12: [0, 0, 0], # (Black) 'Road'
207
+ 13: [102, 102, 255], # (Light Purple) 'Building'
208
+ 14: [255, 204, 153], # (Light Orange) 'No Data'
209
+ }
210
+
211
+ elif color_scheme == 'monochrome':
212
+ return {
213
+ -99: [0, 0, 0], # void
214
+ -30: [28, 28, 99], # 'Landmark'
215
+ -17: [242, 242, 242], # 'plaster'
216
+ -16: [51, 51, 153], # 'glass'
217
+ -15: [102, 102, 204], # 'stone'
218
+ -14: [153, 153, 204], # 'metal'
219
+ -13: [204, 204, 230], # 'concrete'
220
+ -12: [76, 76, 178], # 'wood'
221
+ -11: [25, 25, 127], # 'brick'
222
+ -3: [179, 179, 230], # Building
223
+ -2: [51, 51, 153], # Tree
224
+ -1: [102, 102, 178], # Underground
225
+ 1: [230, 230, 255], # 'Bareland'
226
+ 2: [128, 128, 204], # 'Rangeland'
227
+ 3: [102, 102, 204], # 'Shrub'
228
+ 4: [153, 153, 230], # 'Agriculture land'
229
+ 5: [76, 76, 178], # 'Tree'
230
+ 6: [204, 204, 255], # 'Moss and lichen'
231
+ 7: [76, 76, 178], # 'Wet land'
232
+ 8: [25, 25, 127], # 'Mangrove'
233
+ 9: [51, 51, 204], # 'Water'
234
+ 10: [242, 242, 255], # 'Snow and ice'
235
+ 11: [128, 128, 178], # 'Developed space'
236
+ 12: [51, 51, 127], # 'Road'
237
+ 13: [153, 153, 204], # 'Building'
238
+ 14: [230, 230, 255], # 'No Data'
239
+ }
240
+
241
+ elif color_scheme == 'pastel':
242
+ return {
243
+ -99: [0, 0, 0], # void
244
+ -30: [255, 179, 217], # (Pastel Pink) 'Landmark'
245
+ -17: [245, 245, 245], # (Off White) 'plaster'
246
+ -16: [173, 196, 230], # (Pastel Blue) 'glass'
247
+ -15: [222, 213, 196], # (Pastel Brown) 'stone'
248
+ -14: [211, 219, 226], # (Pastel Gray) 'metal'
249
+ -13: [226, 226, 226], # (Light Gray) 'concrete'
250
+ -12: [255, 223, 179], # (Pastel Orange) 'wood'
251
+ -11: [204, 168, 166], # (Pastel Red) 'brick'
252
+ -3: [214, 217, 235], # (Pastel Purple) Building
253
+ -2: [190, 207, 180], # (Pastel Green) Tree
254
+ -1: [235, 204, 204], # (Pastel Pink) Underground
255
+ 1: [250, 244, 227], # (Cream) 'Bareland'
256
+ 2: [213, 217, 182], # (Pastel Olive) 'Rangeland'
257
+ 3: [200, 226, 195], # (Pastel Green) 'Shrub'
258
+ 4: [209, 214, 188], # (Pastel Yellow-Green) 'Agriculture land'
259
+ 5: [195, 220, 168], # (Light Pastel Green) 'Tree'
260
+ 6: [237, 241, 196], # (Pastel Yellow) 'Moss and lichen'
261
+ 7: [180, 210, 205], # (Pastel Teal) 'Wet land'
262
+ 8: [176, 196, 190], # (Darker Pastel Teal) 'Mangrove'
263
+ 9: [188, 206, 235], # (Pastel Blue) 'Water'
264
+ 10: [242, 245, 250], # (Light Blue-White) 'Snow and ice'
265
+ 11: [209, 213, 219], # (Pastel Gray) 'Developed space'
266
+ 12: [189, 190, 204], # (Pastel Blue-Gray) 'Road'
267
+ 13: [215, 221, 232], # (Very Light Pastel Blue) 'Building'
268
+ 14: [250, 244, 227], # (Cream) 'No Data'
269
+ }
270
+
271
+ elif color_scheme == 'dark_mode':
272
+ return {
273
+ -99: [0, 0, 0], # void
274
+ -30: [153, 51, 102], # (Dark Pink) 'Landmark'
275
+ -17: [76, 76, 76], # (Dark Gray) 'plaster'
276
+ -16: [33, 46, 51], # (Very Dark Blue) 'glass'
277
+ -15: [89, 84, 66], # (Very Dark Brown) 'stone'
278
+ -14: [83, 89, 94], # (Dark Gray) 'metal'
279
+ -13: [61, 61, 61], # (Dark Gray) 'concrete'
280
+ -12: [153, 102, 0], # (Dark Orange) 'wood'
281
+ -11: [51, 35, 33], # (Very Dark Red) 'brick'
282
+ -3: [78, 82, 99], # (Dark Purple) Building
283
+ -2: [46, 58, 37], # (Dark Green) Tree
284
+ -1: [99, 68, 68], # (Dark Pink) Underground
285
+ 1: [102, 97, 75], # (Dark Yellow) 'Bareland'
286
+ 2: [61, 66, 31], # (Dark Olive) 'Rangeland'
287
+ 3: [46, 77, 46], # (Dark Green) 'Shrub'
288
+ 4: [56, 61, 28], # (Dark Yellow-Green) 'Agriculture land'
289
+ 5: [54, 77, 31], # (Dark Green) 'Tree'
290
+ 6: [89, 97, 20], # (Dark Yellow) 'Moss and lichen'
291
+ 7: [38, 59, 49], # (Dark Teal) 'Wet land'
292
+ 8: [16, 31, 26], # (Very Dark Green) 'Mangrove'
293
+ 9: [22, 33, 66], # (Dark Blue) 'Water'
294
+ 10: [82, 87, 92], # (Dark Blue-Gray) 'Snow and ice'
295
+ 11: [46, 51, 56], # (Dark Gray) 'Developed space'
296
+ 12: [25, 31, 43], # (Very Dark Blue) 'Road'
297
+ 13: [56, 64, 82], # (Dark Blue-Gray) 'Building'
298
+ 14: [102, 97, 75], # (Dark Yellow) 'No Data'
299
+ }
300
+
301
+ elif color_scheme == 'grayscale':
302
+ return {
303
+ -99: [0, 0, 0], # void (black)
304
+ -30: [255, 0, 102], # (Pink) 'Landmark',
305
+ -17: [240, 240, 240], # 'plaster'
306
+ -16: [60, 60, 60], # 'glass'
307
+ -15: [130, 130, 130], # 'stone'
308
+ -14: [150, 150, 150], # 'metal'
309
+ -13: [180, 180, 180], # 'concrete'
310
+ -12: [170, 170, 170], # 'wood'
311
+ -11: [70, 70, 70], # 'brick'
312
+ -3: [190, 190, 190], # Building
313
+ -2: [90, 90, 90], # Tree
314
+ -1: [160, 160, 160], # Underground
315
+ 1: [230, 230, 230], # 'Bareland'
316
+ 2: [120, 120, 120], # 'Rangeland'
317
+ 3: [110, 110, 110], # 'Shrub'
318
+ 4: [115, 115, 115], # 'Agriculture land'
319
+ 5: [100, 100, 100], # 'Tree'
320
+ 6: [210, 210, 210], # 'Moss and lichen'
321
+ 7: [95, 95, 95], # 'Wet land'
322
+ 8: [40, 40, 40], # 'Mangrove'
323
+ 9: [50, 50, 50], # 'Water'
324
+ 10: [220, 220, 220], # 'Snow and ice'
325
+ 11: [140, 140, 140], # 'Developed space'
326
+ 12: [30, 30, 30], # 'Road'
327
+ 13: [170, 170, 170], # 'Building'
328
+ 14: [230, 230, 230], # 'No Data'
329
+ }
330
+
331
+ elif color_scheme == 'autumn':
332
+ return {
333
+ -99: [0, 0, 0], # void
334
+ -30: [227, 66, 52], # (Red) 'Landmark'
335
+ -17: [250, 240, 230], # (Antique White) 'plaster'
336
+ -16: [94, 33, 41], # (Dark Red) 'glass'
337
+ -15: [160, 120, 90], # (Medium Brown) 'stone'
338
+ -14: [176, 141, 87], # (Bronze) 'metal'
339
+ -13: [205, 186, 150], # (Tan) 'concrete'
340
+ -12: [204, 85, 0], # (Dark Orange) 'wood'
341
+ -11: [128, 55, 36], # (Rust) 'brick'
342
+ -3: [222, 184, 135], # (Tan) Building
343
+ -2: [107, 68, 35], # (Brown) Tree
344
+ -1: [165, 105, 79], # (Copper) Underground
345
+ 1: [255, 235, 205], # (Blanched Almond) 'Bareland'
346
+ 2: [133, 99, 99], # (Brown) 'Rangeland'
347
+ 3: [139, 69, 19], # (Saddle Brown) 'Shrub'
348
+ 4: [160, 82, 45], # (Sienna) 'Agriculture land'
349
+ 5: [101, 67, 33], # (Dark Brown) 'Tree'
350
+ 6: [255, 228, 196], # (Bisque) 'Moss and lichen'
351
+ 7: [138, 51, 36], # (Rust) 'Wet land'
352
+ 8: [85, 45, 23], # (Deep Brown) 'Mangrove'
353
+ 9: [175, 118, 70], # (Light Brown) 'Water'
354
+ 10: [255, 250, 240], # (Floral White) 'Snow and ice'
355
+ 11: [188, 143, 143], # (Rosy Brown) 'Developed space'
356
+ 12: [69, 41, 33], # (Very Dark Brown) 'Road'
357
+ 13: [210, 180, 140], # (Tan) 'Building'
358
+ 14: [255, 235, 205], # (Blanched Almond) 'No Data'
359
+ }
360
+
361
+ elif color_scheme == 'cool':
362
+ return {
363
+ -99: [0, 0, 0], # void
364
+ -30: [180, 82, 205], # (Purple) 'Landmark'
365
+ -17: [240, 248, 255], # (Alice Blue) 'plaster'
366
+ -16: [70, 130, 180], # (Steel Blue) 'glass'
367
+ -15: [100, 149, 237], # (Cornflower Blue) 'stone'
368
+ -14: [176, 196, 222], # (Light Steel Blue) 'metal'
369
+ -13: [240, 255, 255], # (Azure) 'concrete'
370
+ -12: [65, 105, 225], # (Royal Blue) 'wood'
371
+ -11: [95, 158, 160], # (Cadet Blue) 'brick'
372
+ -3: [135, 206, 235], # (Sky Blue) Building
373
+ -2: [0, 128, 128], # (Teal) Tree
374
+ -1: [127, 255, 212], # (Aquamarine) Underground
375
+ 1: [220, 240, 250], # (Light Blue) 'Bareland'
376
+ 2: [72, 209, 204], # (Medium Turquoise) 'Rangeland'
377
+ 3: [0, 191, 255], # (Deep Sky Blue) 'Shrub'
378
+ 4: [100, 149, 237], # (Cornflower Blue) 'Agriculture land'
379
+ 5: [0, 128, 128], # (Teal) 'Tree'
380
+ 6: [175, 238, 238], # (Pale Turquoise) 'Moss and lichen'
381
+ 7: [32, 178, 170], # (Light Sea Green) 'Wet land'
382
+ 8: [25, 25, 112], # (Midnight Blue) 'Mangrove'
383
+ 9: [30, 144, 255], # (Dodger Blue) 'Water'
384
+ 10: [240, 255, 255], # (Azure) 'Snow and ice'
385
+ 11: [119, 136, 153], # (Light Slate Gray) 'Developed space'
386
+ 12: [25, 25, 112], # (Midnight Blue) 'Road'
387
+ 13: [173, 216, 230], # (Light Blue) 'Building'
388
+ 14: [220, 240, 250], # (Light Blue) 'No Data'
389
+ }
390
+
391
+ elif color_scheme == 'earth_tones':
392
+ return {
393
+ -99: [0, 0, 0], # void
394
+ -30: [210, 105, 30], # (Chocolate) 'Landmark'
395
+ -17: [245, 245, 220], # (Beige) 'plaster'
396
+ -16: [139, 137, 137], # (Gray) 'glass'
397
+ -15: [160, 120, 90], # (Medium Brown) 'stone'
398
+ -14: [169, 169, 169], # (Dark Gray) 'metal'
399
+ -13: [190, 190, 180], # (Light Gray-Tan) 'concrete'
400
+ -12: [160, 82, 45], # (Sienna) 'wood'
401
+ -11: [139, 69, 19], # (Saddle Brown) 'brick'
402
+ -3: [210, 180, 140], # (Tan) Building
403
+ -2: [85, 107, 47], # (Dark Olive Green) Tree
404
+ -1: [133, 94, 66], # (Beaver) Underground
405
+ 1: [222, 184, 135], # (Burlywood) 'Bareland'
406
+ 2: [107, 142, 35], # (Olive Drab) 'Rangeland'
407
+ 3: [85, 107, 47], # (Dark Olive Green) 'Shrub'
408
+ 4: [128, 128, 0], # (Olive) 'Agriculture land'
409
+ 5: [34, 139, 34], # (Forest Green) 'Tree'
410
+ 6: [189, 183, 107], # (Dark Khaki) 'Moss and lichen'
411
+ 7: [143, 188, 143], # (Dark Sea Green) 'Wet land'
412
+ 8: [46, 139, 87], # (Sea Green) 'Mangrove'
413
+ 9: [95, 158, 160], # (Cadet Blue) 'Water'
414
+ 10: [238, 232, 205], # (Light Tan) 'Snow and ice'
415
+ 11: [169, 169, 169], # (Dark Gray) 'Developed space'
416
+ 12: [90, 90, 90], # (Dark Gray) 'Road'
417
+ 13: [188, 170, 152], # (Tan) 'Building'
418
+ 14: [222, 184, 135], # (Burlywood) 'No Data'
419
+ }
420
+
421
+ elif color_scheme == 'vibrant':
422
+ return {
423
+ -99: [0, 0, 0], # void
424
+ -30: [255, 0, 255], # (Magenta) 'Landmark'
425
+ -17: [255, 255, 255], # (White) 'plaster'
426
+ -16: [0, 191, 255], # (Deep Sky Blue) 'glass'
427
+ -15: [255, 215, 0], # (Gold) 'stone'
428
+ -14: [0, 250, 154], # (Medium Spring Green) 'metal'
429
+ -13: [211, 211, 211], # (Light Gray) 'concrete'
430
+ -12: [255, 69, 0], # (Orange Red) 'wood'
431
+ -11: [178, 34, 34], # (Firebrick) 'brick'
432
+ -3: [123, 104, 238], # (Medium Slate Blue) Building
433
+ -2: [50, 205, 50], # (Lime Green) Tree
434
+ -1: [255, 20, 147], # (Deep Pink) Underground
435
+ 1: [255, 255, 0], # (Yellow) 'Bareland'
436
+ 2: [0, 255, 0], # (Lime) 'Rangeland'
437
+ 3: [0, 128, 0], # (Green) 'Shrub'
438
+ 4: [154, 205, 50], # (Yellow Green) 'Agriculture land'
439
+ 5: [34, 139, 34], # (Forest Green) 'Tree'
440
+ 6: [127, 255, 0], # (Chartreuse) 'Moss and lichen'
441
+ 7: [64, 224, 208], # (Turquoise) 'Wet land'
442
+ 8: [0, 100, 0], # (Dark Green) 'Mangrove'
443
+ 9: [0, 0, 255], # (Blue) 'Water'
444
+ 10: [240, 248, 255], # (Alice Blue) 'Snow and ice'
445
+ 11: [128, 128, 128], # (Gray) 'Developed space'
446
+ 12: [47, 79, 79], # (Dark Slate Gray) 'Road'
447
+ 13: [135, 206, 250], # (Light Sky Blue) 'Building'
448
+ 14: [255, 255, 224], # (Light Yellow) 'No Data'
449
+ }
450
+
451
+ # ------------------------------------------------
452
+ # NEWLY ADDED STYLISH COLOR SCHEMES BELOW:
453
+ # ------------------------------------------------
454
+ elif color_scheme == 'cyberpunk':
455
+ """
456
+ Vibrant neon purples, pinks, and blues with deep blacks.
457
+ Think futuristic city vibes and bright neon signs.
458
+ """
459
+ return {
460
+ -99: [0, 0, 0], # void (keep it pitch black)
461
+ -30: [255, 0, 255], # (Neon Magenta) 'Landmark'
462
+ -17: [255, 255, 255], # (Bright White) 'plaster'
463
+ -16: [0, 255, 255], # (Neon Cyan) 'glass'
464
+ -15: [128, 0, 128], # (Purple) 'stone'
465
+ -14: [50, 50, 50], # (Dark Gray) 'metal'
466
+ -13: [102, 0, 102], # (Dark Magenta) 'concrete'
467
+ -12: [255, 20, 147], # (Deep Pink) 'wood'
468
+ -11: [153, 0, 76], # (Deep Purple-Red) 'brick'
469
+ -3: [124, 0, 255], # (Strong Neon Purple) Building
470
+ -2: [0, 255, 153], # (Neon Greenish Cyan) Tree
471
+ -1: [255, 0, 102], # (Hot Pink) Underground
472
+ 1: [255, 255, 153], # (Pale Yellow) 'Bareland'
473
+ 2: [0, 204, 204], # (Teal) 'Rangeland'
474
+ 3: [153, 51, 255], # (Light Purple) 'Shrub'
475
+ 4: [0, 153, 255], # (Bright Neon Blue) 'Agriculture land'
476
+ 5: [0, 255, 153], # (Neon Greenish Cyan) 'Tree'
477
+ 6: [204, 0, 255], # (Vivid Violet) 'Moss and lichen'
478
+ 7: [0, 255, 255], # (Neon Cyan) 'Wet land'
479
+ 8: [0, 102, 102], # (Dark Teal) 'Mangrove'
480
+ 9: [51, 0, 102], # (Deep Indigo) 'Water'
481
+ 10: [255, 255, 255], # (White) 'Snow and ice'
482
+ 11: [102, 102, 102], # (Gray) 'Developed space'
483
+ 12: [0, 0, 0], # (Black) 'Road'
484
+ 13: [204, 51, 255], # (Bright Magenta) 'Building'
485
+ 14: [255, 255, 153], # (Pale Yellow) 'No Data'
486
+ }
487
+
488
+ elif color_scheme == 'tropical':
489
+ """
490
+ Bold, bright 'tropical vacation' color palette.
491
+ Lots of greens, oranges, pinks, reminiscent of island florals.
492
+ """
493
+ return {
494
+ -99: [0, 0, 0], # void
495
+ -30: [255, 99, 164], # (Bright Tropical Pink) 'Landmark'
496
+ -17: [255, 248, 220], # (Cornsilk) 'plaster'
497
+ -16: [0, 150, 136], # (Teal) 'glass'
498
+ -15: [255, 140, 0], # (Dark Orange) 'stone'
499
+ -14: [255, 215, 180], # (Light Peach) 'metal'
500
+ -13: [210, 210, 210], # (Light Gray) 'concrete'
501
+ -12: [255, 165, 0], # (Orange) 'wood'
502
+ -11: [205, 92, 92], # (Indian Red) 'brick'
503
+ -3: [255, 193, 37], # (Tropical Yellow) Building
504
+ -2: [34, 139, 34], # (Forest Green) Tree
505
+ -1: [255, 160, 122], # (Light Salmon) Underground
506
+ 1: [240, 230, 140], # (Khaki) 'Bareland'
507
+ 2: [60, 179, 113], # (Medium Sea Green) 'Rangeland'
508
+ 3: [46, 139, 87], # (Sea Green) 'Shrub'
509
+ 4: [255, 127, 80], # (Coral) 'Agriculture land'
510
+ 5: [50, 205, 50], # (Lime Green) 'Tree'
511
+ 6: [255, 239, 213], # (Papaya Whip) 'Moss and lichen'
512
+ 7: [255, 99, 71], # (Tomato) 'Wet land'
513
+ 8: [47, 79, 79], # (Dark Slate Gray) 'Mangrove'
514
+ 9: [0, 128, 128], # (Teal) 'Water'
515
+ 10: [224, 255, 255], # (Light Cyan) 'Snow and ice'
516
+ 11: [218, 112, 214], # (Orchid) 'Developed space'
517
+ 12: [85, 107, 47], # (Dark Olive Green) 'Road'
518
+ 13: [253, 245, 230], # (Old Lace) 'Building'
519
+ 14: [240, 230, 140], # (Khaki) 'No Data'
520
+ }
521
+
522
+ elif color_scheme == 'vintage':
523
+ """
524
+ A muted, old-photo or sepia-inspired palette
525
+ for a nostalgic or antique look.
526
+ """
527
+ return {
528
+ -99: [0, 0, 0], # void
529
+ -30: [133, 94, 66], # (Beaver/Brownish) 'Landmark'
530
+ -17: [250, 240, 230], # (Antique White) 'plaster'
531
+ -16: [169, 157, 143], # (Muted Brown-Gray) 'glass'
532
+ -15: [181, 166, 127], # (Khaki Tan) 'stone'
533
+ -14: [120, 106, 93], # (Faded Gray-Brown) 'metal'
534
+ -13: [190, 172, 145], # (Light Brown) 'concrete'
535
+ -12: [146, 109, 83], # (Leather Brown) 'wood'
536
+ -11: [125, 80, 70], # (Dusty Brick) 'brick'
537
+ -3: [201, 174, 146], # (Tanned Beige) Building
538
+ -2: [112, 98, 76], # (Faded Olive-Brown) Tree
539
+ -1: [172, 140, 114], # (Light Saddle Brown) Underground
540
+ 1: [222, 202, 166], # (Light Tan) 'Bareland'
541
+ 2: [131, 114, 83], # (Brownish) 'Rangeland'
542
+ 3: [105, 96, 74], # (Dark Olive Brown) 'Shrub'
543
+ 4: [162, 141, 118], # (Beige Brown) 'Agriculture land'
544
+ 5: [95, 85, 65], # (Muted Dark Brown) 'Tree'
545
+ 6: [212, 200, 180], # (Off-White Tan) 'Moss and lichen'
546
+ 7: [140, 108, 94], # (Dusky Mauve-Brown) 'Wet land'
547
+ 8: [85, 73, 60], # (Dark Taupe) 'Mangrove'
548
+ 9: [166, 152, 121], # (Pale Brown) 'Water'
549
+ 10: [250, 245, 235], # (Light Antique White) 'Snow and ice'
550
+ 11: [120, 106, 93], # (Faded Gray-Brown) 'Developed space'
551
+ 12: [77, 66, 55], # (Dark Taupe) 'Road'
552
+ 13: [203, 188, 162], # (Light Warm Gray) 'Building'
553
+ 14: [222, 202, 166], # (Light Tan) 'No Data'
554
+ }
555
+
556
+ elif color_scheme == 'neon_dreams':
557
+ """
558
+ A super-bright, high-energy neon palette.
559
+ Perfect if you want a 'nightclub in 2080' vibe.
560
+ """
561
+ return {
562
+ -99: [0, 0, 0], # void
563
+ -30: [255, 0, 255], # (Magenta) 'Landmark'
564
+ -17: [255, 255, 255], # (White) 'plaster'
565
+ -16: [0, 255, 255], # (Cyan) 'glass'
566
+ -15: [255, 255, 0], # (Yellow) 'stone'
567
+ -14: [0, 255, 0], # (Lime) 'metal'
568
+ -13: [128, 128, 128], # (Gray) 'concrete'
569
+ -12: [255, 165, 0], # (Neon Orange) 'wood'
570
+ -11: [255, 20, 147], # (Deep Pink) 'brick'
571
+ -3: [75, 0, 130], # (Indigo) Building
572
+ -2: [102, 255, 0], # (Bright Lime Green) Tree
573
+ -1: [255, 51, 153], # (Neon Pink) Underground
574
+ 1: [255, 153, 0], # (Bright Orange) 'Bareland'
575
+ 2: [153, 204, 0], # (Vivid Yellow-Green) 'Rangeland'
576
+ 3: [102, 205, 170], # (Aquamarine-ish) 'Shrub'
577
+ 4: [0, 250, 154], # (Medium Spring Green) 'Agriculture land'
578
+ 5: [173, 255, 47], # (Green-Yellow) 'Tree'
579
+ 6: [127, 255, 0], # (Chartreuse) 'Moss and lichen'
580
+ 7: [64, 224, 208], # (Turquoise) 'Wet land'
581
+ 8: [0, 128, 128], # (Teal) 'Mangrove'
582
+ 9: [0, 0, 255], # (Blue) 'Water'
583
+ 10: [224, 255, 255], # (Light Cyan) 'Snow and ice'
584
+ 11: [192, 192, 192], # (Silver) 'Developed space'
585
+ 12: [25, 25, 25], # (Near Black) 'Road'
586
+ 13: [75, 0, 130], # (Indigo) 'Building'
587
+ 14: [255, 153, 0], # (Bright Orange) 'No Data'
588
+ }
589
+
590
+ else:
591
+ # If an unknown color scheme is specified, return the default
592
+ print(f"Unknown color scheme '{color_scheme}'. Using default instead.")
593
+ return get_voxel_color_map('default')
594
+
595
+ def visualize_3d_voxel(voxel_grid, voxel_color_map = 'default', voxel_size=2.0, save_path=None):
596
+ """
597
+ Visualizes 3D voxel data using matplotlib's 3D plotting capabilities.
598
+
599
+ This function creates a 3D visualization of voxel data where each non-zero voxel
600
+ is rendered as a colored cube. The colors are determined by the voxel values
601
+ and the specified color scheme. The visualization includes proper transparency
602
+ handling and aspect ratio adjustment.
603
+
604
+ Parameters:
605
+ -----------
606
+ voxel_grid : numpy.ndarray
607
+ 3D numpy array containing voxel data. Shape should be (x, y, z) where
608
+ each element represents a voxel class ID. Zero values are treated as empty space.
609
+
610
+ voxel_color_map : str, optional
611
+ Name of the color scheme to use for voxel coloring. Default is 'default'.
612
+ See get_voxel_color_map() for available options.
613
+
614
+ voxel_size : float, optional
615
+ Physical size of each voxel in meters. Used for z-axis scaling and labels.
616
+ Default is 2.0 meters.
617
+
618
+ save_path : str, optional
619
+ File path to save the generated plot. If None, the plot is only displayed.
620
+ Default is None.
621
+
622
+ Returns:
623
+ --------
624
+ None
625
+ The function displays the plot and optionally saves it to file.
626
+
627
+ Notes:
628
+ ------
629
+ - Void voxels (value -99) are rendered transparent
630
+ - Underground voxels (value -1) and trees (value -2) have reduced transparency
631
+ - Z-axis ticks are automatically scaled to show real-world heights
632
+ - The plot aspect ratio is adjusted to maintain proper voxel proportions
633
+ - For large voxel grids, this function may be slow due to matplotlib's 3D rendering
634
+
635
+ Examples:
636
+ ---------
637
+ >>> # Basic visualization
638
+ >>> visualize_3d_voxel(voxel_array)
639
+
640
+ >>> # Use cyberpunk color scheme and save to file
641
+ >>> visualize_3d_voxel(voxel_array, 'cyberpunk', save_path='city_view.png')
642
+
643
+ >>> # Adjust voxel size for different scale
644
+ >>> visualize_3d_voxel(voxel_array, voxel_size=1.0)
645
+ """
646
+ # Get the color mapping for the specified scheme
647
+ color_map = get_voxel_color_map(voxel_color_map)
648
+
649
+ print("\tVisualizing 3D voxel data")
650
+ # Create a figure and a 3D axis
651
+ fig = plt.figure(figsize=(12, 10))
652
+ ax = fig.add_subplot(111, projection='3d')
653
+
654
+ print("\tProcessing voxels...")
655
+ # Create boolean mask for voxels that should be rendered (non-zero values)
656
+ filled_voxels = voxel_grid != 0
657
+
658
+ # Initialize color array with RGBA values (Red, Green, Blue, Alpha)
659
+ colors = np.zeros(voxel_grid.shape + (4,)) # RGBA
660
+
661
+ # Process each possible voxel value and assign colors
662
+ for val in range(-99, 15): # Updated range to include -3 and -2
663
+ # Create mask for voxels with this specific value
664
+ mask = voxel_grid == val
665
+
666
+ if val in color_map:
667
+ # Convert RGB values from [0,255] to [0,1] range for matplotlib
668
+ rgb = [x/255 for x in color_map[val]] # Normalize RGB values to [0, 1]
669
+
670
+ # Set transparency based on voxel type
671
+ # alpha = 0.7 if ((val == -1) or (val == -2)) else 0.9 # More transparent for underground and below
672
+ alpha = 0.0 if (val == -99) else 1 # Void voxels are completely transparent
673
+ # alpha = 1
674
+
675
+ # Assign RGBA color to all voxels of this type
676
+ colors[mask] = rgb + [alpha]
677
+ else:
678
+ # Default color for undefined voxel types
679
+ colors[mask] = [0, 0, 0, 0.9] # Default color if not in color_map
680
+
681
+ # Render voxels with progress bar
682
+ with tqdm(total=np.prod(voxel_grid.shape)) as pbar:
683
+ ax.voxels(filled_voxels, facecolors=colors, edgecolors=None)
684
+ pbar.update(np.prod(voxel_grid.shape))
685
+
686
+ # print("Finalizing plot...")
687
+ # Set labels and title
688
+ # ax.set_xlabel('X')
689
+ # ax.set_ylabel('Y')
690
+ # ax.set_zlabel('Z (meters)')
691
+ # ax.set_title('3D Voxel Visualization')
692
+
693
+ # Configure z-axis ticks to show meaningful height values
694
+ # Adjust z-axis ticks to show every 10 cells or less
695
+ z_max = voxel_grid.shape[2]
696
+ if z_max <= 10:
697
+ z_ticks = range(0, z_max + 1)
698
+ else:
699
+ z_ticks = range(0, z_max + 1, 10)
700
+
701
+ # Remove axes for cleaner appearance
702
+ ax.axis('off')
703
+ # ax.set_zticks(z_ticks)
704
+ # ax.set_zticklabels([f"{z * voxel_size:.1f}" for z in z_ticks])
705
+
706
+ # Set aspect ratio to be equal for realistic proportions
707
+ max_range = np.array([voxel_grid.shape[0], voxel_grid.shape[1], voxel_grid.shape[2]]).max()
708
+ ax.set_box_aspect((voxel_grid.shape[0]/max_range, voxel_grid.shape[1]/max_range, voxel_grid.shape[2]/max_range))
709
+
710
+ # Set z-axis limits to focus on the relevant height range
711
+ ax.set_zlim(bottom=0)
712
+ ax.set_zlim(top=150)
713
+
714
+ # print("Visualization complete. Displaying plot...")
715
+ plt.tight_layout()
716
+
717
+ # Save plot if path is provided
718
+ if save_path:
719
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
720
+ plt.show()
721
+
722
+ def visualize_3d_voxel_plotly(voxel_grid, voxel_color_map = 'default', voxel_size=2.0):
723
+ """
724
+ Creates an interactive 3D visualization of voxel data using Plotly.
725
+
726
+ This function generates an interactive 3D visualization using Plotly's Mesh3d
727
+ and Scatter3d objects. Each voxel is rendered as a cube with proper lighting
728
+ and edge visualization. The resulting plot supports interactive rotation,
729
+ zooming, and panning.
730
+
731
+ Parameters:
732
+ -----------
733
+ voxel_grid : numpy.ndarray
734
+ 3D numpy array containing voxel data. Shape should be (x, y, z) where
735
+ each element represents a voxel class ID. Zero values are treated as empty space.
736
+
737
+ voxel_color_map : str, optional
738
+ Name of the color scheme to use for voxel coloring. Default is 'default'.
739
+ See get_voxel_color_map() for available options.
740
+
741
+ voxel_size : float, optional
742
+ Physical size of each voxel in meters. Used for z-axis scaling and labels.
743
+ Default is 2.0 meters.
744
+
745
+ Returns:
746
+ --------
747
+ None
748
+ The function displays the interactive plot in the browser or notebook.
749
+
750
+ Notes:
751
+ ------
752
+ - Creates individual cube geometries for each non-zero voxel
753
+ - Includes edge lines for better visual definition
754
+ - Uses orthographic projection for technical visualization
755
+ - May be slow for very large voxel grids due to individual cube generation
756
+ - Lighting is optimized for clear visualization of building structures
757
+
758
+ Technical Details:
759
+ ------------------
760
+ - Each cube is defined by 8 vertices and 12 triangular faces
761
+ - Edge lines are generated separately for visual clarity
762
+ - Color mapping follows the same convention as matplotlib version
763
+ - Camera is positioned for isometric view with orthographic projection
764
+
765
+ Examples:
766
+ ---------
767
+ >>> # Basic interactive visualization
768
+ >>> visualize_3d_voxel_plotly(voxel_array)
769
+
770
+ >>> # Use high contrast colors for better visibility
771
+ >>> visualize_3d_voxel_plotly(voxel_array, 'high_contrast')
772
+
773
+ >>> # Adjust scale for different voxel sizes
774
+ >>> visualize_3d_voxel_plotly(voxel_array, voxel_size=1.0)
775
+ """
776
+ # Get the color mapping for the specified scheme
777
+ color_map = get_voxel_color_map(voxel_color_map)
778
+
779
+ print("Preparing visualization...")
780
+
781
+ print("Processing voxels...")
782
+ # Initialize lists to store mesh data
783
+ x, y, z = [], [], [] # Vertex coordinates
784
+ i, j, k = [], [], [] # Face indices (triangles)
785
+ colors = [] # Vertex colors
786
+ edge_x, edge_y, edge_z = [], [], [] # Edge line coordinates
787
+ vertex_index = 0 # Current vertex index counter
788
+
789
+ # Define cube faces using vertex indices
790
+ # Each cube has 12 triangular faces (2 per square face)
791
+ cube_i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
792
+ cube_j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
793
+ cube_k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
794
+
795
+ # Process each voxel in the grid
796
+ with tqdm(total=np.prod(voxel_grid.shape)) as pbar:
797
+ for xi in range(voxel_grid.shape[0]):
798
+ for yi in range(voxel_grid.shape[1]):
799
+ for zi in range(voxel_grid.shape[2]):
800
+ # Only process non-zero voxels
801
+ if voxel_grid[xi, yi, zi] != 0:
802
+ # Define the 8 vertices of a unit cube at this position
803
+ # Vertices are ordered: bottom face (z), then top face (z+1)
804
+ cube_vertices = [
805
+ [xi, yi, zi], [xi+1, yi, zi], [xi+1, yi+1, zi], [xi, yi+1, zi], # Bottom face
806
+ [xi, yi, zi+1], [xi+1, yi, zi+1], [xi+1, yi+1, zi+1], [xi, yi+1, zi+1] # Top face
807
+ ]
808
+
809
+ # Add vertex coordinates to the mesh data
810
+ x.extend([v[0] for v in cube_vertices])
811
+ y.extend([v[1] for v in cube_vertices])
812
+ z.extend([v[2] for v in cube_vertices])
813
+
814
+ # Add face indices (offset by current vertex_index)
815
+ i.extend([x + vertex_index for x in cube_i])
816
+ j.extend([x + vertex_index for x in cube_j])
817
+ k.extend([x + vertex_index for x in cube_k])
818
+
819
+ # Get color for this voxel type and replicate for all 8 vertices
820
+ color = color_map.get(voxel_grid[xi, yi, zi], [0, 0, 0])
821
+ colors.extend([color] * 8)
822
+
823
+ # Generate edge lines for visual clarity
824
+ # Define the 12 edges of a cube (4 bottom + 4 top + 4 vertical)
825
+ edges = [
826
+ (0,1), (1,2), (2,3), (3,0), # Bottom face edges
827
+ (4,5), (5,6), (6,7), (7,4), # Top face edges
828
+ (0,4), (1,5), (2,6), (3,7) # Vertical edges
829
+ ]
830
+
831
+ # Add edge coordinates (None creates line breaks between edges)
832
+ for start, end in edges:
833
+ edge_x.extend([cube_vertices[start][0], cube_vertices[end][0], None])
834
+ edge_y.extend([cube_vertices[start][1], cube_vertices[end][1], None])
835
+ edge_z.extend([cube_vertices[start][2], cube_vertices[end][2], None])
836
+
837
+ # Increment vertex index for next cube
838
+ vertex_index += 8
839
+ pbar.update(1)
840
+
841
+ print("Creating Plotly figure...")
842
+ # Create 3D mesh object with vertices, faces, and colors
843
+ mesh = go.Mesh3d(
844
+ x=x, y=y, z=z,
845
+ i=i, j=j, k=k,
846
+ vertexcolor=colors,
847
+ opacity=1,
848
+ flatshading=True,
849
+ name='Voxel Grid'
850
+ )
851
+
852
+ # Configure lighting for better visualization
853
+ # Add lighting to the mesh
854
+ mesh.update(
855
+ lighting=dict(ambient=0.7, # Ambient light (overall brightness)
856
+ diffuse=1, # Diffuse light (surface shading)
857
+ fresnel=0.1, # Fresnel effect (edge highlighting)
858
+ specular=1, # Specular highlights
859
+ roughness=0.05, # Surface roughness
860
+ facenormalsepsilon=1e-15,
861
+ vertexnormalsepsilon=1e-15),
862
+ lightposition=dict(x=100, # Light source position
863
+ y=200,
864
+ z=0)
865
+ )
866
+
867
+ # Create edge lines for better visual definition
868
+ edges = go.Scatter3d(
869
+ x=edge_x, y=edge_y, z=edge_z,
870
+ mode='lines',
871
+ line=dict(color='lightgrey', width=1),
872
+ name='Edges'
873
+ )
874
+
875
+ # Combine mesh and edges into a figure
876
+ fig = go.Figure(data=[mesh, edges])
877
+
878
+ # Configure plot layout and camera settings
879
+ # Set labels, title, and use orthographic projection
880
+ fig.update_layout(
881
+ scene=dict(
882
+ xaxis_title='X',
883
+ yaxis_title='Y',
884
+ zaxis_title='Z (meters)',
885
+ aspectmode='data', # Maintain data aspect ratios
886
+ camera=dict(
887
+ projection=dict(type="orthographic") # Use orthographic projection
888
+ )
889
+ ),
890
+ title='3D Voxel Visualization'
891
+ )
892
+
893
+ # Configure z-axis to show real-world heights
894
+ # Adjust z-axis ticks to show every 10 cells or less
895
+ z_max = voxel_grid.shape[2]
896
+ if z_max <= 10:
897
+ z_ticks = list(range(0, z_max + 1))
898
+ else:
899
+ z_ticks = list(range(0, z_max + 1, 10))
900
+
901
+ # Update z-axis with meaningful height labels
902
+ fig.update_layout(
903
+ scene=dict(
904
+ zaxis=dict(
905
+ tickvals=z_ticks,
906
+ ticktext=[f"{z * voxel_size:.1f}" for z in z_ticks]
907
+ )
908
+ )
909
+ )
910
+
911
+ print("Visualization complete. Displaying plot...")
912
+ fig.show()
913
+
914
+ def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light', **kwargs):
915
+ """
916
+ Core function for plotting 2D grid data overlaid on basemaps.
917
+
918
+ This function handles the visualization of various types of grid data by creating
919
+ colored polygons for each grid cell and overlaying them on a web basemap. It supports
920
+ different data types with appropriate color schemes and handles special values like
921
+ NaN and zero appropriately.
922
+
923
+ Parameters:
924
+ -----------
925
+ grid : numpy.ndarray
926
+ 2D array containing the grid data values to visualize.
927
+
928
+ origin : numpy.ndarray
929
+ Geographic coordinates [lon, lat] of the grid's origin point.
930
+
931
+ adjusted_meshsize : float
932
+ Size of each grid cell in meters after grid size adjustments.
933
+
934
+ u_vec, v_vec : numpy.ndarray
935
+ Unit vectors defining the grid orientation in geographic space.
936
+
937
+ transformer : pyproj.Transformer
938
+ Coordinate transformer for converting between geographic and projected coordinates.
939
+
940
+ vertices : list
941
+ List of [lon, lat] coordinates defining the grid boundary.
942
+
943
+ data_type : str
944
+ Type of data being visualized. Supported types:
945
+ - 'land_cover': Land use/land cover classifications
946
+ - 'building_height': Building height values
947
+ - 'dem': Digital elevation model
948
+ - 'canopy_height': Vegetation height
949
+ - 'green_view_index': Green visibility index
950
+ - 'sky_view_index': Sky visibility factor
951
+
952
+ vmin, vmax : float, optional
953
+ Min/max values for color scaling. Auto-calculated if not provided.
954
+
955
+ color_map : str, optional
956
+ Matplotlib colormap name to override default schemes.
957
+
958
+ alpha : float, optional
959
+ Transparency of grid overlay (0-1). Default is 0.5.
960
+
961
+ buf : float, optional
962
+ Buffer around grid for plot extent as fraction of grid size. Default is 0.2.
963
+
964
+ edge : bool, optional
965
+ Whether to draw cell edges. Default is True.
966
+
967
+ basemap : str, optional
968
+ Basemap style name. Default is 'CartoDB light'.
969
+
970
+ **kwargs : dict
971
+ Additional parameters specific to data types:
972
+ - land_cover_classes: Dictionary for land cover data
973
+ - buildings: List of building polygons for building_height data
974
+
975
+ Returns:
976
+ --------
977
+ None
978
+ Displays the plot with matplotlib.
979
+
980
+ Notes:
981
+ ------
982
+ - Grid is transposed to match geographic orientation
983
+ - Special handling for NaN, zero, and negative values depending on data type
984
+ - Basemap is added using contextily for geographic context
985
+ - Plot extent is automatically calculated from grid vertices
986
+ """
987
+ # Create matplotlib figure and axis
988
+ fig, ax = plt.subplots(figsize=(12, 12))
989
+
990
+ # Configure visualization parameters based on data type
991
+ if data_type == 'land_cover':
992
+ # Land cover uses discrete color categories
993
+ land_cover_classes = kwargs.get('land_cover_classes')
994
+ colors = [mcolors.to_rgb(f'#{r:02x}{g:02x}{b:02x}') for r, g, b in land_cover_classes.keys()]
995
+ cmap = mcolors.ListedColormap(colors)
996
+ norm = mcolors.BoundaryNorm(range(len(land_cover_classes)+1), cmap.N)
997
+ title = 'Grid Cells with Dominant Land Cover Classes'
998
+ label = 'Land Cover Class'
999
+ tick_labels = list(land_cover_classes.values())
1000
+
1001
+ elif data_type == 'building_height':
1002
+ # Building height uses continuous colormap with special handling for zero/NaN
1003
+ # Create a masked array to handle special values
1004
+ masked_grid = np.ma.masked_array(grid, mask=(np.isnan(grid) | (grid == 0)))
1005
+
1006
+ # Set up colormap and normalization for positive values
1007
+ cmap = plt.cm.viridis
1008
+ if vmin is None:
1009
+ vmin = np.nanmin(masked_grid[masked_grid > 0])
1010
+ if vmax is None:
1011
+ vmax = np.nanmax(masked_grid)
1012
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1013
+
1014
+ title = 'Grid Cells with Building Heights'
1015
+ label = 'Building Height (m)'
1016
+ tick_labels = None
1017
+
1018
+ elif data_type == 'dem':
1019
+ # Digital elevation model uses terrain colormap
1020
+ cmap = plt.cm.terrain
1021
+ if vmin is None:
1022
+ vmin = np.nanmin(grid)
1023
+ if vmax is None:
1024
+ vmax = np.nanmax(grid)
1025
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1026
+ title = 'DEM Grid Overlaid on Map'
1027
+ label = 'Elevation (m)'
1028
+ tick_labels = None
1029
+ elif data_type == 'canopy_height':
1030
+ cmap = plt.cm.Greens
1031
+ if vmin is None:
1032
+ vmin = np.nanmin(grid)
1033
+ if vmax is None:
1034
+ vmax = np.nanmax(grid)
1035
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1036
+ title = 'Canopy Height Grid Overlaid on Map'
1037
+ label = 'Canopy Height (m)'
1038
+ tick_labels = None
1039
+ elif data_type == 'green_view_index':
1040
+ cmap = plt.cm.Greens
1041
+ if vmin is None:
1042
+ vmin = np.nanmin(grid)
1043
+ if vmax is None:
1044
+ vmax = np.nanmax(grid)
1045
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1046
+ title = 'Green View Index Grid Overlaid on Map'
1047
+ label = 'Green View Index'
1048
+ tick_labels = None
1049
+ elif data_type == 'sky_view_index':
1050
+ cmap = plt.cm.get_cmap('BuPu_r').copy()
1051
+ if vmin is None:
1052
+ vmin = np.nanmin(grid)
1053
+ if vmax is None:
1054
+ vmax = np.nanmax(grid)
1055
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1056
+ title = 'Sky View Index Grid Overlaid on Map'
1057
+ label = 'Sky View Index'
1058
+ tick_labels = None
1059
+ else:
1060
+ cmap = plt.cm.viridis
1061
+ if vmin is None:
1062
+ vmin = np.nanmin(grid)
1063
+ if vmax is None:
1064
+ vmax = np.nanmax(grid)
1065
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1066
+ tick_labels = None
1067
+
1068
+ if color_map:
1069
+ # cmap = plt.cm.get_cmap(color_map).copy()
1070
+ cmap = sns.color_palette(color_map, as_cmap=True).copy()
1071
+
1072
+ # Ensure grid is in the correct orientation
1073
+ grid = grid.T
1074
+
1075
+ for i in range(grid.shape[0]):
1076
+ for j in range(grid.shape[1]):
1077
+ cell = create_cell_polygon(origin, j, i, adjusted_meshsize, u_vec, v_vec) # Note the swap of i and j
1078
+ x, y = cell.exterior.xy
1079
+ x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
1080
+
1081
+ value = grid[i, j]
1082
+
1083
+ if data_type == 'building_height':
1084
+ if np.isnan(value):
1085
+ # White fill for NaN values
1086
+ ax.fill(x, y, alpha=alpha, fc='gray', ec='black' if edge else None, linewidth=0.1)
1087
+ elif value == 0:
1088
+ # No fill for zero values, only edges if enabled
1089
+ if edge:
1090
+ ax.plot(x, y, color='black', linewidth=0.1)
1091
+ elif value > 0:
1092
+ # Viridis colormap for positive values
1093
+ color = cmap(norm(value))
1094
+ ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
1095
+ elif data_type == 'canopy_height':
1096
+ color = cmap(norm(value))
1097
+ if value == 0:
1098
+ # No fill for zero values, only edges if enabled
1099
+ if edge:
1100
+ ax.plot(x, y, color='black', linewidth=0.1)
1101
+ else:
1102
+ if edge:
1103
+ ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
1104
+ else:
1105
+ ax.fill(x, y, alpha=alpha, fc=color, ec=None)
1106
+ elif 'view' in data_type:
1107
+ if np.isnan(value):
1108
+ # No fill for zero values, only edges if enabled
1109
+ if edge:
1110
+ ax.plot(x, y, color='black', linewidth=0.1)
1111
+ elif value >= 0:
1112
+ # Viridis colormap for positive values
1113
+ color = cmap(norm(value))
1114
+ ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
1115
+ else:
1116
+ color = cmap(norm(value))
1117
+ if edge:
1118
+ ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
1119
+ else:
1120
+ ax.fill(x, y, alpha=alpha, fc=color, ec=None)
1121
+
1122
+ crs_epsg_3857 = CRS.from_epsg(3857)
1123
+
1124
+ basemaps = {
1125
+ 'CartoDB dark': ctx.providers.CartoDB.DarkMatter, # Popular dark option
1126
+ 'CartoDB light': ctx.providers.CartoDB.Positron, # Popular dark option
1127
+ 'CartoDB voyager': ctx.providers.CartoDB.Voyager, # Popular dark option
1128
+ 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels, # Popular dark option
1129
+ 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1130
+ }
1131
+ ctx.add_basemap(ax, crs=crs_epsg_3857, source=basemaps[basemap])
1132
+ # if basemap == "dark":
1133
+ # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.DarkMatter)
1134
+ # elif basemap == 'light':
1135
+ # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.Positron)
1136
+ # elif basemap == 'voyager':
1137
+ # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.Voyager)
1138
+
1139
+ if data_type == 'building_height':
1140
+ buildings = kwargs.get('buildings', [])
1141
+ for building in buildings:
1142
+ polygon = Polygon(building['geometry']['coordinates'][0])
1143
+ x, y = polygon.exterior.xy
1144
+ x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
1145
+ ax.plot(x, y, color='red', linewidth=1.5)
1146
+ # print(polygon)
1147
+
1148
+ # Safe calculation of plot limits
1149
+ all_coords = np.array(vertices)
1150
+ x, y = zip(*[transformer.transform(lon, lat) for lat, lon in all_coords])
1151
+
1152
+ # Calculate limits safely
1153
+ x_min, x_max = min(x), max(x)
1154
+ y_min, y_max = min(y), max(y)
1155
+
1156
+ if x_min != x_max and y_min != y_max and buf != 0:
1157
+ dist_x = x_max - x_min
1158
+ dist_y = y_max - y_min
1159
+ # Set limits with buffer
1160
+ ax.set_xlim(x_min - buf * dist_x, x_max + buf * dist_x)
1161
+ ax.set_ylim(y_min - buf * dist_y, y_max + buf * dist_y)
1162
+ else:
1163
+ # If coordinates are the same or buffer is 0, set limits without buffer
1164
+ ax.set_xlim(x_min, x_max)
1165
+ ax.set_ylim(y_min, y_max)
1166
+
1167
+ plt.axis('off')
1168
+ plt.tight_layout()
1169
+ plt.show()
1170
+
1171
+ def display_builing_ids_on_map(building_geojson, rectangle_vertices):
1172
+ """
1173
+ Creates an interactive folium map displaying building footprints with selectable IDs.
1174
+
1175
+ This function generates a web map showing building polygons within a circular area
1176
+ around the center of the specified rectangle. Each building is labeled with its
1177
+ ID and additional information, making it easy to identify specific buildings
1178
+ for analysis or selection.
1179
+
1180
+ Parameters:
1181
+ -----------
1182
+ building_geojson : list
1183
+ List of GeoJSON feature dictionaries representing building polygons.
1184
+ Each feature should have:
1185
+ - 'geometry': GeoJSON polygon geometry
1186
+ - 'properties': Dictionary with 'id' and optional 'name' fields
1187
+
1188
+ rectangle_vertices : list
1189
+ List of [lat, lon] coordinate pairs defining the area of interest.
1190
+ Used to calculate the map center and intersection area.
1191
+
1192
+ Returns:
1193
+ --------
1194
+ folium.Map
1195
+ Interactive folium map object with building polygons and labels.
1196
+
1197
+ Notes:
1198
+ ------
1199
+ - Only buildings intersecting with a 200m radius circle are displayed
1200
+ - Building IDs are displayed as selectable text labels
1201
+ - Map is automatically centered on the rectangle vertices
1202
+ - Popup information includes building ID and name (if available)
1203
+ - Building polygons are styled with blue color and semi-transparent fill
1204
+
1205
+ Examples:
1206
+ ---------
1207
+ >>> vertices = [[40.7580, -73.9855], [40.7590, -73.9855],
1208
+ ... [40.7590, -73.9845], [40.7580, -73.9845]]
1209
+ >>> buildings = [{'geometry': {...}, 'properties': {'id': '123', 'name': 'Building A'}}]
1210
+ >>> map_obj = display_builing_ids_on_map(buildings, vertices)
1211
+ >>> map_obj.save('buildings_map.html')
1212
+ """
1213
+ # Parse the GeoJSON data
1214
+ geojson_data = building_geojson
1215
+
1216
+ # Calculate the center point of the rectangle for map centering
1217
+ # Extract all latitudes and longitudes
1218
+ lats = [coord[0] for coord in rectangle_vertices]
1219
+ lons = [coord[1] for coord in rectangle_vertices]
1220
+
1221
+ # Calculate center by averaging min and max values
1222
+ center_lat = (min(lats) + max(lats)) / 2
1223
+ center_lon = (min(lons) + max(lons)) / 2
1224
+
1225
+ # Create circle polygon for intersection testing (200m radius)
1226
+ circle = create_circle_polygon(center_lat, center_lon, 200)
1227
+
1228
+ # Create a map centered on the data
1229
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=17)
1230
+
1231
+ # Process each building feature
1232
+ # Add building footprints to the map
1233
+ for feature in geojson_data:
1234
+ # Convert coordinates if needed
1235
+ coords = convert_coordinates(feature['geometry']['coordinates'][0])
1236
+ building_polygon = Polygon(coords)
1237
+
1238
+ # Only process buildings that intersect with the circular area
1239
+ # Check if building intersects with circle
1240
+ if building_polygon.intersects(circle):
1241
+ # Extract building information from properties
1242
+ # Get and format building properties
1243
+ # building_id = format_building_id(feature['properties'].get('id', 0))
1244
+ building_id = str(feature['properties'].get('id', 0))
1245
+ building_name = feature['properties'].get('name:en',
1246
+ feature['properties'].get('name', f'Building {building_id}'))
1247
+
1248
+ # Create popup content with selectable ID
1249
+ popup_content = f"""
1250
+ <div>
1251
+ Building ID: <span style="user-select: all">{building_id}</span><br>
1252
+ Name: {building_name}
1253
+ </div>
1254
+ """
1255
+
1256
+ # Add building polygon to map with popup information
1257
+ # Add polygon to map
1258
+ folium.Polygon(
1259
+ locations=coords,
1260
+ popup=folium.Popup(popup_content),
1261
+ color='blue',
1262
+ weight=2,
1263
+ fill=True,
1264
+ fill_color='blue',
1265
+ fill_opacity=0.2
1266
+ ).add_to(m)
1267
+
1268
+ # Add building ID label at the polygon centroid
1269
+ # Calculate centroid for label placement
1270
+ centroid = calculate_centroid(coords)
1271
+
1272
+ # Add building ID as a selectable label
1273
+ folium.Marker(
1274
+ centroid,
1275
+ icon=folium.DivIcon(
1276
+ html=f'''
1277
+ <div style="
1278
+ position: relative;
1279
+ font-family: monospace;
1280
+ font-size: 12px;
1281
+ color: black;
1282
+ background-color: rgba(255, 255, 255, 0.9);
1283
+ padding: 5px 8px;
1284
+ margin: -10px -15px;
1285
+ border: 1px solid black;
1286
+ border-radius: 4px;
1287
+ user-select: all;
1288
+ cursor: text;
1289
+ white-space: nowrap;
1290
+ display: inline-block;
1291
+ box-shadow: 0 0 3px rgba(0,0,0,0.2);
1292
+ ">{building_id}</div>
1293
+ ''',
1294
+ class_name="building-label"
1295
+ )
1296
+ ).add_to(m)
1297
+
1298
+ # Save the map
1299
+ return m
1300
+
1301
+ def visualize_land_cover_grid_on_map(grid, rectangle_vertices, meshsize, source = 'Urbanwatch', vmin=None, vmax=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1302
+ """
1303
+ Visualizes land cover classification grid overlaid on a basemap.
1304
+
1305
+ This function creates a map visualization of land cover data using predefined
1306
+ color schemes for different land cover classes. Each grid cell is colored
1307
+ according to its dominant land cover type and overlaid on a web basemap
1308
+ for geographic context.
1309
+
1310
+ Parameters:
1311
+ -----------
1312
+ grid : numpy.ndarray
1313
+ 2D array containing land cover class indices. Values should correspond
1314
+ to indices in the land cover classification system.
1315
+
1316
+ rectangle_vertices : list
1317
+ List of [lon, lat] coordinate pairs defining the grid boundary corners.
1318
+ Should contain exactly 4 vertices in geographic coordinates.
1319
+
1320
+ meshsize : float
1321
+ Target size of each grid cell in meters. May be adjusted during processing.
1322
+
1323
+ source : str, optional
1324
+ Source of land cover classification system. Default is 'Urbanwatch'.
1325
+ See get_land_cover_classes() for available options.
1326
+
1327
+ vmin, vmax : float, optional
1328
+ Not used for land cover (discrete categories). Included for API consistency.
1329
+
1330
+ alpha : float, optional
1331
+ Transparency of grid overlay (0-1). Default is 0.5.
1332
+
1333
+ buf : float, optional
1334
+ Buffer around grid for plot extent as fraction of grid size. Default is 0.2.
1335
+
1336
+ edge : bool, optional
1337
+ Whether to draw cell edges. Default is True.
1338
+
1339
+ basemap : str, optional
1340
+ Basemap style name. Options include:
1341
+ - 'CartoDB light' (default)
1342
+ - 'CartoDB dark'
1343
+ - 'CartoDB voyager'
1344
+ Default is 'CartoDB light'.
1345
+
1346
+ Returns:
1347
+ --------
1348
+ None
1349
+ Displays the plot and prints information about unique land cover classes.
1350
+
1351
+ Notes:
1352
+ ------
1353
+ - Grid coordinates are calculated using geodetic calculations
1354
+ - Land cover classes are mapped to predefined colors
1355
+ - Unique classes present in the grid are printed for reference
1356
+ - Uses Web Mercator projection (EPSG:3857) for basemap compatibility
1357
+
1358
+ Examples:
1359
+ ---------
1360
+ >>> # Basic land cover visualization
1361
+ >>> vertices = [[lon1, lat1], [lon2, lat2], [lon3, lat3], [lon4, lat4]]
1362
+ >>> visualize_land_cover_grid_on_map(lc_grid, vertices, 10.0)
1363
+
1364
+ >>> # With custom styling
1365
+ >>> visualize_land_cover_grid_on_map(lc_grid, vertices, 10.0,
1366
+ ... alpha=0.7, edge=False,
1367
+ ... basemap='CartoDB dark')
1368
+ """
1369
+ # Initialize geodetic calculator for distance measurements
1370
+ geod = initialize_geod()
1371
+
1372
+ # Get land cover class definitions and colors
1373
+ land_cover_classes = get_land_cover_classes(source)
1374
+
1375
+ # Extract key vertices for grid calculations
1376
+ vertex_0 = rectangle_vertices[0]
1377
+ vertex_1 = rectangle_vertices[1]
1378
+ vertex_3 = rectangle_vertices[3]
1379
+
1380
+ # Calculate distances between vertices using geodetic calculations
1381
+ dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1382
+ dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1383
+
1384
+ # Calculate side vectors in geographic coordinates
1385
+ side_1 = np.array(vertex_1) - np.array(vertex_0)
1386
+ side_2 = np.array(vertex_3) - np.array(vertex_0)
1387
+
1388
+ # Create normalized unit vectors for grid orientation
1389
+ u_vec = normalize_to_one_meter(side_1, dist_side_1)
1390
+ v_vec = normalize_to_one_meter(side_2, dist_side_2)
1391
+
1392
+ # Set grid origin and calculate optimal grid size
1393
+ origin = np.array(rectangle_vertices[0])
1394
+ grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1395
+
1396
+ print(f"Calculated grid size: {grid_size}")
1397
+ # print(f"Adjusted mesh size: {adjusted_meshsize}")
1398
+
1399
+ # Set up coordinate transformation for basemap compatibility
1400
+ geotiff_crs = CRS.from_epsg(3857)
1401
+ transformer = setup_transformer(CRS.from_epsg(4326), geotiff_crs)
1402
+
1403
+ # Generate grid cell coordinates (not currently used but available for advanced processing)
1404
+ cell_coords = create_coordinate_mesh(origin, grid_size, adjusted_meshsize, u_vec, v_vec)
1405
+ cell_coords_flat = cell_coords.reshape(2, -1).T
1406
+ transformed_coords = np.array([transform_coords(transformer, lon, lat) for lat, lon in cell_coords_flat])
1407
+ transformed_coords = transformed_coords.reshape(grid_size[::-1] + (2,))
1408
+
1409
+ # print(f"Grid shape: {grid.shape}")
1410
+
1411
+ # Create the visualization using the general plot_grid function
1412
+ plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1413
+ rectangle_vertices, 'land_cover', alpha=alpha, buf=buf, edge=edge, basemap=basemap, land_cover_classes=land_cover_classes)
1414
+
1415
+ # Display information about the land cover classes present in the grid
1416
+ unique_indices = np.unique(grid)
1417
+ unique_classes = [list(land_cover_classes.values())[i] for i in unique_indices]
1418
+ # print(f"Unique classes in the grid: {unique_classes}")
1419
+
1420
+ def visualize_building_height_grid_on_map(building_height_grid, filtered_buildings, rectangle_vertices, meshsize, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1421
+ # Calculate grid and normalize vectors
1422
+ geod = initialize_geod()
1423
+ vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
1424
+
1425
+ dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1426
+ dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1427
+
1428
+ side_1 = np.array(vertex_1) - np.array(vertex_0)
1429
+ side_2 = np.array(vertex_3) - np.array(vertex_0)
1430
+
1431
+ u_vec = normalize_to_one_meter(side_1, dist_side_1)
1432
+ v_vec = normalize_to_one_meter(side_2, dist_side_2)
1433
+
1434
+ origin = np.array(rectangle_vertices[0])
1435
+ _, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1436
+
1437
+ # Setup transformer and plotting extent
1438
+ transformer = setup_transformer(CRS.from_epsg(4326), CRS.from_epsg(3857))
1439
+
1440
+ # Plot the results
1441
+ plot_grid(building_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1442
+ rectangle_vertices, 'building_height', vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap, buildings=filtered_buildings)
1443
+
1444
+ def visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, meshsize, type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1445
+ # Calculate grid and normalize vectors
1446
+ geod = initialize_geod()
1447
+ vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
1448
+
1449
+ dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1450
+ dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1451
+
1452
+ side_1 = np.array(vertex_1) - np.array(vertex_0)
1453
+ side_2 = np.array(vertex_3) - np.array(vertex_0)
1454
+
1455
+ u_vec = normalize_to_one_meter(side_1, dist_side_1)
1456
+ v_vec = normalize_to_one_meter(side_2, dist_side_2)
1457
+
1458
+ origin = np.array(rectangle_vertices[0])
1459
+ _, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1460
+
1461
+ # Setup transformer and plotting extent
1462
+ transformer = setup_transformer(CRS.from_epsg(4326), CRS.from_epsg(3857))
1463
+
1464
+ # Plot the results
1465
+ plot_grid(canopy_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1466
+ rectangle_vertices, type, vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap)
1467
+
1468
+ def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
1469
+ all_classes = list(land_cover_classes.values())
1470
+ unique_classes = list(dict.fromkeys(all_classes)) # Preserve order and remove duplicates
1471
+
1472
+ colors = [color_map[cls] for cls in unique_classes]
1473
+ cmap = mcolors.ListedColormap(colors)
1474
+
1475
+ bounds = np.arange(len(unique_classes) + 1)
1476
+ norm = mcolors.BoundaryNorm(bounds, cmap.N)
1477
+
1478
+ class_to_num = {cls: i for i, cls in enumerate(unique_classes)}
1479
+ numeric_grid = np.vectorize(class_to_num.get)(grid)
1480
+
1481
+ plt.figure(figsize=(10, 10))
1482
+ im = plt.imshow(numeric_grid, cmap=cmap, norm=norm, interpolation='nearest')
1483
+ cbar = plt.colorbar(im, ticks=bounds[:-1] + 0.5)
1484
+ cbar.set_ticklabels(unique_classes)
1485
+ plt.title(f'Land Use/Land Cover Grid (Mesh Size: {mesh_size}m)')
1486
+ plt.xlabel('Grid Cells (X)')
1487
+ plt.ylabel('Grid Cells (Y)')
1488
+ plt.show()
1489
+
1490
+ def visualize_numerical_grid(grid, mesh_size, title, cmap='viridis', label='Value', vmin=None, vmax=None):
1491
+ plt.figure(figsize=(10, 10))
1492
+ plt.imshow(grid, cmap=cmap, vmin=vmin, vmax=vmax)
1493
+ plt.colorbar(label=label)
1494
+ plt.title(f'{title} (Mesh Size: {mesh_size}m)')
1495
+ plt.xlabel('Grid Cells (X)')
1496
+ plt.ylabel('Grid Cells (Y)')
1497
+ plt.show()
1498
+
1499
+ def convert_coordinates(coords):
1500
+ return coords
1501
+
1502
+ def calculate_centroid(coords):
1503
+ lat_sum = sum(coord[0] for coord in coords)
1504
+ lon_sum = sum(coord[1] for coord in coords)
1505
+ return [lat_sum / len(coords), lon_sum / len(coords)]
1506
+
1507
+ def calculate_center(features):
1508
+ lats = []
1509
+ lons = []
1510
+ for feature in features:
1511
+ coords = feature['geometry']['coordinates'][0]
1512
+ for lat, lon in coords:
1513
+ lats.append(lat)
1514
+ lons.append(lon)
1515
+ return sum(lats) / len(lats), sum(lons) / len(lons)
1516
+
1517
+ def create_circle_polygon(center_lat, center_lon, radius_meters):
1518
+ """Create a circular polygon with given center and radius"""
1519
+ # Convert radius from meters to degrees (approximate)
1520
+ radius_deg = radius_meters / 111000 # 1 degree ≈ 111km at equator
1521
+
1522
+ # Create circle points
1523
+ points = []
1524
+ for angle in range(361): # 0 to 360 degrees
1525
+ rad = math.radians(angle)
1526
+ lat = center_lat + (radius_deg * math.cos(rad))
1527
+ lon = center_lon + (radius_deg * math.sin(rad) / math.cos(math.radians(center_lat)))
1528
+ points.append((lat, lon))
1529
+ return Polygon(points)
1530
+
1531
+ def visualize_landcover_grid_on_basemap(landcover_grid, rectangle_vertices, meshsize, source='Standard', alpha=0.6, figsize=(12, 8),
1532
+ basemap='CartoDB light', show_edge=False, edge_color='black', edge_width=0.5):
1533
+ """Visualizes a land cover grid GeoDataFrame using predefined color schemes.
1534
+
1535
+ Args:
1536
+ gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1537
+ source: Source of land cover classification (e.g., 'Standard', 'Urbanwatch', etc.)
1538
+ title: Title for the plot (default: None)
1539
+ alpha: Transparency of the grid overlay (default: 0.6)
1540
+ figsize: Figure size in inches (default: (12, 8))
1541
+ basemap: Basemap style (default: 'CartoDB light')
1542
+ show_edge: Whether to show cell edges (default: True)
1543
+ edge_color: Color of cell edges (default: 'black')
1544
+ edge_width: Width of cell edges (default: 0.5)
1545
+ """
1546
+ # Get land cover classes and colors
1547
+ land_cover_classes = get_land_cover_classes(source)
1548
+
1549
+ gdf = grid_to_geodataframe(landcover_grid, rectangle_vertices, meshsize)
1550
+
1551
+ # Convert RGB tuples to normalized RGB values
1552
+ colors = [(r/255, g/255, b/255) for (r,g,b) in land_cover_classes.keys()]
1553
+
1554
+ # Create custom colormap
1555
+ cmap = ListedColormap(colors)
1556
+
1557
+ # Create bounds for discrete colorbar
1558
+ bounds = np.arange(len(colors) + 1)
1559
+ norm = BoundaryNorm(bounds, cmap.N)
1560
+
1561
+ # Convert to Web Mercator
1562
+ gdf_web = gdf.to_crs(epsg=3857)
1563
+
1564
+ # Create figure and axis
1565
+ fig, ax = plt.subplots(figsize=figsize)
1566
+
1567
+ # Plot the GeoDataFrame
1568
+ gdf_web.plot(column='value',
1569
+ ax=ax,
1570
+ alpha=alpha,
1571
+ cmap=cmap,
1572
+ norm=norm,
1573
+ legend=True,
1574
+ legend_kwds={
1575
+ 'label': 'Land Cover Class',
1576
+ 'ticks': bounds[:-1] + 0.5,
1577
+ 'boundaries': bounds,
1578
+ 'format': lambda x, p: list(land_cover_classes.values())[int(x)]
1579
+ },
1580
+ edgecolor=edge_color if show_edge else 'none',
1581
+ linewidth=edge_width if show_edge else 0)
1582
+
1583
+ # Add basemap
1584
+ basemaps = {
1585
+ 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1586
+ 'CartoDB light': ctx.providers.CartoDB.Positron,
1587
+ 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1588
+ 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1589
+ 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1590
+ }
1591
+ ctx.add_basemap(ax, source=basemaps[basemap])
1592
+
1593
+ # Set title and remove axes
1594
+ ax.set_axis_off()
1595
+
1596
+ plt.tight_layout()
1597
+ plt.show()
1598
+
1599
+ def visualize_numerical_grid_on_basemap(grid, rectangle_vertices, meshsize, value_name="value", cmap='viridis', vmin=None, vmax=None,
1600
+ alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1601
+ show_edge=False, edge_color='black', edge_width=0.5):
1602
+ """Visualizes a numerical grid GeoDataFrame (e.g., heights) on a basemap.
1603
+
1604
+ Args:
1605
+ gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1606
+ title: Title for the plot (default: None)
1607
+ cmap: Colormap to use (default: 'viridis')
1608
+ vmin: Minimum value for colormap scaling (default: None)
1609
+ vmax: Maximum value for colormap scaling (default: None)
1610
+ alpha: Transparency of the grid overlay (default: 0.6)
1611
+ figsize: Figure size in inches (default: (12, 8))
1612
+ basemap: Basemap style (default: 'CartoDB light')
1613
+ show_edge: Whether to show cell edges (default: True)
1614
+ edge_color: Color of cell edges (default: 'black')
1615
+ edge_width: Width of cell edges (default: 0.5)
1616
+ """
1617
+
1618
+ gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
1619
+
1620
+ # Convert to Web Mercator
1621
+ gdf_web = gdf.to_crs(epsg=3857)
1622
+
1623
+ # Create figure and axis
1624
+ fig, ax = plt.subplots(figsize=figsize)
1625
+
1626
+ # Plot the GeoDataFrame
1627
+ gdf_web.plot(column='value',
1628
+ ax=ax,
1629
+ alpha=alpha,
1630
+ cmap=cmap,
1631
+ vmin=vmin,
1632
+ vmax=vmax,
1633
+ legend=True,
1634
+ legend_kwds={'label': value_name},
1635
+ edgecolor=edge_color if show_edge else 'none',
1636
+ linewidth=edge_width if show_edge else 0)
1637
+
1638
+ # Add basemap
1639
+ basemaps = {
1640
+ 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1641
+ 'CartoDB light': ctx.providers.CartoDB.Positron,
1642
+ 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1643
+ 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1644
+ 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1645
+ }
1646
+ ctx.add_basemap(ax, source=basemaps[basemap])
1647
+
1648
+ # Set title and remove axes
1649
+ ax.set_axis_off()
1650
+
1651
+ plt.tight_layout()
1652
+ plt.show()
1653
+
1654
+ def visualize_numerical_gdf_on_basemap(gdf, value_name="value", cmap='viridis', vmin=None, vmax=None,
1655
+ alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1656
+ show_edge=False, edge_color='black', edge_width=0.5):
1657
+ """Visualizes a GeoDataFrame with numerical values on a basemap.
1658
+
1659
+ Args:
1660
+ gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1661
+ value_name: Name of the value column and legend label (default: "value")
1662
+ cmap: Colormap to use (default: 'viridis')
1663
+ vmin: Minimum value for colormap scaling (default: None)
1664
+ vmax: Maximum value for colormap scaling (default: None)
1665
+ alpha: Transparency of the grid overlay (default: 0.6)
1666
+ figsize: Figure size in inches (default: (12, 8))
1667
+ basemap: Basemap style (default: 'CartoDB light')
1668
+ show_edge: Whether to show cell edges (default: False)
1669
+ edge_color: Color of cell edges (default: 'black')
1670
+ edge_width: Width of cell edges (default: 0.5)
1671
+ """
1672
+ # Convert to Web Mercator if not already in that CRS
1673
+ if gdf.crs != 'EPSG:3857':
1674
+ gdf_web = gdf.to_crs(epsg=3857)
1675
+ else:
1676
+ gdf_web = gdf
1677
+
1678
+ # Create figure and axis
1679
+ fig, ax = plt.subplots(figsize=figsize)
1680
+
1681
+ # Plot the GeoDataFrame
1682
+ gdf_web.plot(column=value_name,
1683
+ ax=ax,
1684
+ alpha=alpha,
1685
+ cmap=cmap,
1686
+ vmin=vmin,
1687
+ vmax=vmax,
1688
+ legend=True,
1689
+ legend_kwds={'label': value_name},
1690
+ edgecolor=edge_color if show_edge else 'none',
1691
+ linewidth=edge_width if show_edge else 0)
1692
+
1693
+ # Add basemap
1694
+ basemaps = {
1695
+ 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1696
+ 'CartoDB light': ctx.providers.CartoDB.Positron,
1697
+ 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1698
+ 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1699
+ 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1700
+ }
1701
+ ctx.add_basemap(ax, source=basemaps[basemap])
1702
+
1703
+ # Set title and remove axes
1704
+ ax.set_axis_off()
1705
+
1706
+ plt.tight_layout()
1707
+ plt.show()
1708
+
1709
+ def visualize_point_gdf_on_basemap(point_gdf, value_name='value', **kwargs):
1710
+ """Visualizes a point GeoDataFrame on a basemap with colors based on values.
1711
+
1712
+ Args:
1713
+ point_gdf: GeoDataFrame with point geometries and values
1714
+ value_name: Name of the column containing values to visualize (default: 'value')
1715
+ **kwargs: Optional visualization parameters including:
1716
+ - figsize: Tuple for figure size (default: (12, 8))
1717
+ - colormap: Matplotlib colormap name (default: 'viridis')
1718
+ - markersize: Size of points (default: 20)
1719
+ - alpha: Transparency of points (default: 0.7)
1720
+ - vmin: Minimum value for colormap scaling (default: None)
1721
+ - vmax: Maximum value for colormap scaling (default: None)
1722
+ - title: Plot title (default: None)
1723
+ - basemap_style: Contextily basemap style (default: CartoDB.Positron)
1724
+ - zoom: Basemap zoom level (default: 15)
1725
+
1726
+ Returns:
1727
+ matplotlib figure and axis objects
1728
+ """
1729
+ import matplotlib.pyplot as plt
1730
+ import contextily as ctx
1731
+
1732
+ # Set default parameters
1733
+ defaults = {
1734
+ 'figsize': (12, 8),
1735
+ 'colormap': 'viridis',
1736
+ 'markersize': 20,
1737
+ 'alpha': 0.7,
1738
+ 'vmin': None,
1739
+ 'vmax': None,
1740
+ 'title': None,
1741
+ 'basemap_style': ctx.providers.CartoDB.Positron,
1742
+ 'zoom': 15
1743
+ }
1744
+
1745
+ # Update defaults with provided kwargs
1746
+ settings = {**defaults, **kwargs}
1747
+
1748
+ # Create figure and axis
1749
+ fig, ax = plt.subplots(figsize=settings['figsize'])
1750
+
1751
+ # Convert to Web Mercator for basemap compatibility
1752
+ point_gdf_web = point_gdf.to_crs(epsg=3857)
1753
+
1754
+ # Plot points
1755
+ scatter = point_gdf_web.plot(
1756
+ column=value_name,
1757
+ ax=ax,
1758
+ cmap=settings['colormap'],
1759
+ markersize=settings['markersize'],
1760
+ alpha=settings['alpha'],
1761
+ vmin=settings['vmin'],
1762
+ vmax=settings['vmax'],
1763
+ legend=True,
1764
+ legend_kwds={
1765
+ 'label': value_name,
1766
+ 'orientation': 'vertical',
1767
+ 'shrink': 0.8
1768
+ }
1769
+ )
1770
+
1771
+ # Add basemap
1772
+ ctx.add_basemap(
1773
+ ax,
1774
+ source=settings['basemap_style'],
1775
+ zoom=settings['zoom']
1776
+ )
1777
+
1778
+ # Set title if provided
1779
+ if settings['title']:
1780
+ plt.title(settings['title'])
1781
+
1782
+ # Remove axes
1783
+ ax.set_axis_off()
1784
+
1785
+ # Adjust layout to prevent colorbar cutoff
1786
+ plt.tight_layout()
1787
+ plt.show()
1788
+
1789
+ def create_multi_view_scene(meshes, output_directory="output", projection_type="perspective", distance_factor=1.0):
1790
+ """
1791
+ Creates multiple rendered views of 3D city meshes from different camera angles.
1792
+
1793
+ This function generates a comprehensive set of views including isometric and
1794
+ orthographic projections of the 3D city model. Each view is rendered as a
1795
+ high-quality image and saved to the specified directory.
1796
+
1797
+ Parameters:
1798
+ -----------
1799
+ meshes : dict
1800
+ Dictionary mapping mesh names/IDs to trimesh.Trimesh objects.
1801
+ Each mesh represents a different component of the city model.
1802
+
1803
+ output_directory : str, optional
1804
+ Directory path where rendered images will be saved. Default is "output".
1805
+
1806
+ projection_type : str, optional
1807
+ Camera projection type. Options:
1808
+ - "perspective": Natural perspective projection (default)
1809
+ - "orthographic": Technical orthographic projection
1810
+
1811
+ distance_factor : float, optional
1812
+ Multiplier for camera distance from the scene. Default is 1.0.
1813
+ Higher values move camera further away, lower values bring it closer.
1814
+
1815
+ Returns:
1816
+ --------
1817
+ list of tuple
1818
+ List of (view_name, filename) pairs for each generated image.
1819
+
1820
+ Notes:
1821
+ ------
1822
+ - Generates 9 different views: 4 isometric + 5 orthographic
1823
+ - Isometric views: front-right, front-left, back-right, back-left
1824
+ - Orthographic views: top, front, back, left, right
1825
+ - Uses PyVista for high-quality rendering with proper lighting
1826
+ - Camera positions are automatically calculated based on scene bounds
1827
+ - Images are saved as PNG files with high DPI
1828
+
1829
+ Technical Details:
1830
+ ------------------
1831
+ - Scene bounding box is computed from all mesh vertices
1832
+ - Camera distance is scaled based on scene diagonal
1833
+ - Orthographic projection uses parallel scaling for technical drawings
1834
+ - Each view uses optimized lighting for clarity
1835
+
1836
+ Examples:
1837
+ ---------
1838
+ >>> meshes = {'buildings': building_mesh, 'ground': ground_mesh}
1839
+ >>> views = create_multi_view_scene(meshes, "renders/", "orthographic", 1.5)
1840
+ >>> print(f"Generated {len(views)} views")
1841
+ """
1842
+ # Compute overall bounding box across all meshes
1843
+ vertices_list = [mesh.vertices for mesh in meshes.values()]
1844
+ all_vertices = np.vstack(vertices_list)
1845
+ bbox = np.array([
1846
+ [all_vertices[:, 0].min(), all_vertices[:, 1].min(), all_vertices[:, 2].min()],
1847
+ [all_vertices[:, 0].max(), all_vertices[:, 1].max(), all_vertices[:, 2].max()]
1848
+ ])
1849
+
1850
+ # Compute the center and diagonal of the bounding box
1851
+ center = (bbox[1] + bbox[0]) / 2
1852
+ diagonal = np.linalg.norm(bbox[1] - bbox[0])
1853
+
1854
+ # Adjust distance based on projection type
1855
+ if projection_type.lower() == "orthographic":
1856
+ distance = diagonal * 5 # Increase distance for orthographic to capture full scene
1857
+ else:
1858
+ distance = diagonal * 1.8 * distance_factor # Original distance for perspective
1859
+
1860
+ # Define the isometric viewing angles
1861
+ iso_angles = {
1862
+ 'iso_front_right': (1, 1, 0.7),
1863
+ 'iso_front_left': (-1, 1, 0.7),
1864
+ 'iso_back_right': (1, -1, 0.7),
1865
+ 'iso_back_left': (-1, -1, 0.7)
1866
+ }
1867
+
1868
+ # Compute camera positions for isometric views
1869
+ camera_positions = {}
1870
+ for name, direction in iso_angles.items():
1871
+ direction = np.array(direction)
1872
+ direction = direction / np.linalg.norm(direction)
1873
+ camera_pos = center + direction * distance
1874
+ camera_positions[name] = [camera_pos, center, (0, 0, 1)]
1875
+
1876
+ # Add orthographic views
1877
+ ortho_views = {
1878
+ 'xy_top': [center + np.array([0, 0, distance]), center, (-1, 0, 0)],
1879
+ 'yz_right': [center + np.array([distance, 0, 0]), center, (0, 0, 1)],
1880
+ 'xz_front': [center + np.array([0, distance, 0]), center, (0, 0, 1)],
1881
+ 'yz_left': [center + np.array([-distance, 0, 0]), center, (0, 0, 1)],
1882
+ 'xz_back': [center + np.array([0, -distance, 0]), center, (0, 0, 1)]
1883
+ }
1884
+ camera_positions.update(ortho_views)
1885
+
1886
+ images = []
1887
+ for view_name, camera_pos in camera_positions.items():
1888
+ # Create new plotter for each view
1889
+ plotter = pv.Plotter(notebook=True, off_screen=True)
1890
+
1891
+ # Set the projection type
1892
+ if projection_type.lower() == "orthographic":
1893
+ plotter.enable_parallel_projection()
1894
+ # Set parallel scale to ensure the whole scene is visible
1895
+ plotter.camera.parallel_scale = diagonal * 0.4 * distance_factor # Adjust this factor as needed
1896
+
1897
+ elif projection_type.lower() != "perspective":
1898
+ print(f"Warning: Unknown projection_type '{projection_type}'. Using perspective projection.")
1899
+
1900
+ # Add each mesh to the scene
1901
+ for class_id, mesh in meshes.items():
1902
+ vertices = mesh.vertices
1903
+ faces = np.hstack([[3, *face] for face in mesh.faces])
1904
+ pv_mesh = pv.PolyData(vertices, faces)
1905
+
1906
+ if hasattr(mesh.visual, 'face_colors'):
1907
+ colors = mesh.visual.face_colors
1908
+ if colors.max() > 1:
1909
+ colors = colors / 255.0
1910
+ pv_mesh.cell_data['colors'] = colors
1911
+
1912
+ plotter.add_mesh(pv_mesh,
1913
+ rgb=True,
1914
+ scalars='colors' if hasattr(mesh.visual, 'face_colors') else None)
1915
+
1916
+ # Set camera position for this view
1917
+ plotter.camera_position = camera_pos
1918
+
1919
+ # Save screenshot
1920
+ filename = f'{output_directory}/city_view_{view_name}.png'
1921
+ plotter.screenshot(filename)
1922
+ images.append((view_name, filename))
1923
+ plotter.close()
1924
+
1925
+ return images
1926
+
1927
+ def visualize_voxcity_multi_view(voxel_array, meshsize, **kwargs):
1928
+ """
1929
+ Creates comprehensive 3D visualizations of voxel city data with multiple viewing angles.
1930
+
1931
+ This is the primary function for generating publication-quality renderings of voxel
1932
+ city models. It converts voxel data to 3D meshes, optionally overlays simulation
1933
+ results, and produces multiple rendered views from different camera positions.
1934
+
1935
+ Parameters:
1936
+ -----------
1937
+ voxel_array : numpy.ndarray
1938
+ 3D array containing voxel class IDs. Shape should be (x, y, z).
1939
+
1940
+ meshsize : float
1941
+ Physical size of each voxel in meters.
1942
+
1943
+ **kwargs : dict
1944
+ Optional visualization parameters:
1945
+
1946
+ Color and Style:
1947
+ - voxel_color_map (str): Color scheme name, default 'default'
1948
+ - output_directory (str): Directory for output files, default 'output'
1949
+ - output_file_name (str): Base name for exported files
1950
+
1951
+ Simulation Overlay:
1952
+ - sim_grid (numpy.ndarray): 2D simulation results to overlay
1953
+ - dem_grid (numpy.ndarray): Digital elevation model for height reference
1954
+ - view_point_height (float): Height offset for simulation surface, default 1.5m
1955
+ - colormap (str): Matplotlib colormap for simulation data, default 'viridis'
1956
+ - vmin, vmax (float): Color scale limits for simulation data
1957
+
1958
+ Camera and Rendering:
1959
+ - projection_type (str): 'perspective' or 'orthographic', default 'perspective'
1960
+ - distance_factor (float): Camera distance multiplier, default 1.0
1961
+ - window_width, window_height (int): Render resolution, default 1024x768
1962
+
1963
+ Output Control:
1964
+ - show_views (bool): Whether to display rendered views, default True
1965
+ - save_obj (bool): Whether to export OBJ mesh files, default False
1966
+
1967
+ Returns:
1968
+ --------
1969
+ None
1970
+ Displays rendered views and optionally saves files to disk.
1971
+
1972
+ Notes:
1973
+ ------
1974
+ - Automatically configures PyVista for headless rendering
1975
+ - Generates meshes for each voxel class with appropriate colors
1976
+ - Creates colorbar for simulation data if provided
1977
+ - Produces 9 different camera views (4 isometric + 5 orthographic)
1978
+ - Exports mesh files in OBJ format if requested
1979
+
1980
+ Technical Requirements:
1981
+ -----------------------
1982
+ - Requires Xvfb for headless rendering on Linux systems
1983
+ - Uses PyVista for high-quality 3D rendering
1984
+ - Simulation data is interpolated onto elevated surface mesh
1985
+
1986
+ Examples:
1987
+ ---------
1988
+ >>> # Basic visualization
1989
+ >>> visualize_voxcity_multi_view(voxel_array, meshsize=2.0)
1990
+
1991
+ >>> # With simulation results overlay
1992
+ >>> visualize_voxcity_multi_view(
1993
+ ... voxel_array, 2.0,
1994
+ ... sim_grid=temperature_data,
1995
+ ... dem_grid=elevation_data,
1996
+ ... colormap='plasma',
1997
+ ... output_file_name='temperature_analysis'
1998
+ ... )
1999
+
2000
+ >>> # High-resolution orthographic technical drawings
2001
+ >>> visualize_voxcity_multi_view(
2002
+ ... voxel_array, 2.0,
2003
+ ... projection_type='orthographic',
2004
+ ... window_width=2048,
2005
+ ... window_height=1536,
2006
+ ... save_obj=True
2007
+ ... )
2008
+ """
2009
+ # Set up headless rendering environment for PyVista
2010
+ os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2011
+ os.environ['DISPLAY'] = ':99'
2012
+
2013
+ # Configure PyVista settings for high-quality rendering
2014
+ pv.set_plot_theme('document')
2015
+ pv.global_theme.background = 'white'
2016
+ pv.global_theme.window_size = [1024, 768]
2017
+ pv.global_theme.jupyter_backend = 'static'
2018
+
2019
+ # Parse visualization parameters from kwargs
2020
+ voxel_color_map = kwargs.get("voxel_color_map", 'default')
2021
+ vox_dict = get_voxel_color_map(voxel_color_map)
2022
+ output_directory = kwargs.get("output_directory", 'output')
2023
+ base_filename = kwargs.get("output_file_name", None)
2024
+ sim_grid = kwargs.get("sim_grid", None)
2025
+ dem_grid_ori = kwargs.get("dem_grid", None)
2026
+
2027
+ # Normalize DEM grid to start from zero elevation
2028
+ if dem_grid_ori is not None:
2029
+ dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2030
+
2031
+ # Simulation overlay parameters
2032
+ z_offset = kwargs.get("view_point_height", 1.5)
2033
+ cmap_name = kwargs.get("colormap", "viridis")
2034
+ vmin = kwargs.get("vmin", np.nanmin(sim_grid) if sim_grid is not None else None)
2035
+ vmax = kwargs.get("vmax", np.nanmax(sim_grid) if sim_grid is not None else None)
2036
+
2037
+ # Camera and rendering parameters
2038
+ projection_type = kwargs.get("projection_type", "perspective")
2039
+ distance_factor = kwargs.get("distance_factor", 1.0)
2040
+
2041
+ # Output control parameters
2042
+ save_obj = kwargs.get("save_obj", False)
2043
+ show_views = kwargs.get("show_views", True)
2044
+
2045
+ # Create 3D meshes from voxel data
2046
+ print("Creating voxel meshes...")
2047
+ meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2048
+
2049
+ # Add simulation results as elevated surface mesh if provided
2050
+ if sim_grid is not None and dem_grid is not None:
2051
+ print("Creating sim_grid surface mesh...")
2052
+ sim_mesh = create_sim_surface_mesh(
2053
+ sim_grid, dem_grid,
2054
+ meshsize=meshsize,
2055
+ z_offset=z_offset,
2056
+ cmap_name=cmap_name,
2057
+ vmin=vmin,
2058
+ vmax=vmax
2059
+ )
2060
+ if sim_mesh is not None:
2061
+ meshes["sim_surface"] = sim_mesh
2062
+
2063
+ # Create and display colorbar for simulation data
2064
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2065
+ scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2066
+
2067
+ fig, ax = plt.subplots(figsize=(6, 1))
2068
+ plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2069
+ plt.tight_layout()
2070
+ plt.show()
2071
+
2072
+ # Export mesh files if requested
2073
+ if base_filename is not None:
2074
+ print(f"Exporting files to '{base_filename}.*' ...")
2075
+ os.makedirs(output_directory, exist_ok=True)
2076
+ export_meshes(meshes, output_directory, base_filename)
2077
+
2078
+ # Generate and display multiple camera views
2079
+ if show_views:
2080
+ print("Creating multiple views...")
2081
+ os.makedirs(output_directory, exist_ok=True)
2082
+ image_files = create_multi_view_scene(meshes, output_directory=output_directory, projection_type=projection_type, distance_factor=distance_factor)
2083
+
2084
+ # Display each rendered view
2085
+ for view_name, img_file in image_files:
2086
+ plt.figure(figsize=(24, 16))
2087
+ img = plt.imread(img_file)
2088
+ plt.imshow(img)
2089
+ plt.title(view_name.replace('_', ' ').title(), pad=20)
2090
+ plt.axis('off')
2091
+ plt.show()
2092
+ plt.close()
2093
+
2094
+ # Export OBJ mesh files if requested
2095
+ if save_obj:
2096
+ output_directory = kwargs.get('output_directory', 'output')
2097
+ output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2098
+ obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2099
+ print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2100
+
2101
+ def visualize_voxcity_multi_view_with_multiple_sim_grids(voxel_array, meshsize, sim_configs, **kwargs):
2102
+ """
2103
+ Create multiple views of the voxel city data with multiple simulation grids.
2104
+
2105
+ Args:
2106
+ voxel_array: 3D numpy array containing voxel data
2107
+ meshsize: Size of each voxel/cell
2108
+ sim_configs: List of dictionaries, each containing configuration for a simulation grid:
2109
+ {
2110
+ 'sim_grid': 2D numpy array of simulation values,
2111
+ 'z_offset': height offset in meters (default: 1.5),
2112
+ 'cmap_name': colormap name (default: 'viridis'),
2113
+ 'vmin': minimum value for colormap (optional),
2114
+ 'vmax': maximum value for colormap (optional),
2115
+ 'label': label for the colorbar (optional)
2116
+ }
2117
+ **kwargs: Additional arguments including:
2118
+ - vox_dict: Dictionary mapping voxel values to colors
2119
+ - output_directory: Directory to save output images
2120
+ - output_file_name: Base filename for exports
2121
+ - dem_grid: DEM grid for height information
2122
+ - projection_type: 'perspective' or 'orthographic'
2123
+ - distance_factor: Factor to adjust camera distance
2124
+ """
2125
+ os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2126
+ os.environ['DISPLAY'] = ':99'
2127
+
2128
+ # Configure PyVista settings
2129
+ pv.set_plot_theme('document')
2130
+ pv.global_theme.background = 'white'
2131
+ window_width = kwargs.get("window_width", 1024)
2132
+ window_height = kwargs.get("window_height", 768)
2133
+ pv.global_theme.window_size = [window_width, window_height]
2134
+ pv.global_theme.jupyter_backend = 'static'
2135
+
2136
+ # Parse general kwargs
2137
+ voxel_color_map = kwargs.get("voxel_color_map", 'default')
2138
+ vox_dict = get_voxel_color_map(voxel_color_map)
2139
+ output_directory = kwargs.get("output_directory", 'output')
2140
+ base_filename = kwargs.get("output_file_name", None)
2141
+ dem_grid_ori = kwargs.get("dem_grid", None)
2142
+ projection_type = kwargs.get("projection_type", "perspective")
2143
+ distance_factor = kwargs.get("distance_factor", 1.0)
2144
+ show_views = kwargs.get("show_views", True)
2145
+ save_obj = kwargs.get("save_obj", False)
2146
+
2147
+ if dem_grid_ori is not None:
2148
+ dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2149
+
2150
+ # Create meshes
2151
+ print("Creating voxel meshes...")
2152
+ meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2153
+
2154
+ # Process each simulation grid
2155
+ for i, config in enumerate(sim_configs):
2156
+ sim_grid = config['sim_grid']
2157
+ if sim_grid is None or dem_grid is None:
2158
+ continue
2159
+
2160
+ z_offset = config.get('z_offset', 1.5)
2161
+ cmap_name = config.get('cmap_name', 'viridis')
2162
+ vmin = config.get('vmin', np.nanmin(sim_grid))
2163
+ vmax = config.get('vmax', np.nanmax(sim_grid))
2164
+ label = config.get('label', f'Simulation {i+1}')
2165
+
2166
+ print(f"Creating sim_grid surface mesh for {label}...")
2167
+ sim_mesh = create_sim_surface_mesh(
2168
+ sim_grid, dem_grid,
2169
+ meshsize=meshsize,
2170
+ z_offset=z_offset,
2171
+ cmap_name=cmap_name,
2172
+ vmin=vmin,
2173
+ vmax=vmax
2174
+ )
2175
+
2176
+ if sim_mesh is not None:
2177
+ meshes[f"sim_surface_{i}"] = sim_mesh
2178
+
2179
+ # Create colorbar for this simulation
2180
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2181
+ scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2182
+
2183
+ fig, ax = plt.subplots(figsize=(6, 1))
2184
+ plt.colorbar(scalar_map, cax=ax, orientation='horizontal', label=label)
2185
+ plt.tight_layout()
2186
+ plt.show()
2187
+
2188
+ # Export if filename provided
2189
+ if base_filename is not None:
2190
+ print(f"Exporting files to '{base_filename}.*' ...")
2191
+ os.makedirs(output_directory, exist_ok=True)
2192
+ export_meshes(meshes, output_directory, base_filename)
2193
+
2194
+ if show_views:
2195
+ # Create and save multiple views
2196
+ print("Creating multiple views...")
2197
+ os.makedirs(output_directory, exist_ok=True)
2198
+ image_files = create_multi_view_scene(
2199
+ meshes,
2200
+ output_directory=output_directory,
2201
+ projection_type=projection_type,
2202
+ distance_factor=distance_factor
2203
+ )
2204
+
2205
+ # Display each view separately
2206
+ for view_name, img_file in image_files:
2207
+ plt.figure(figsize=(24, 16))
2208
+ img = plt.imread(img_file)
2209
+ plt.imshow(img)
2210
+ plt.title(view_name.replace('_', ' ').title(), pad=20)
2211
+ plt.axis('off')
2212
+ plt.show()
2213
+ plt.close()
2214
+
2215
+ # After creating the meshes and before visualization
2216
+ if save_obj:
2217
+ output_directory = kwargs.get('output_directory', 'output')
2218
+ output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2219
+ obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2220
+ print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2221
+
2222
+ def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None, **kwargs):
2223
+ """
2224
+ Creates 3D visualizations of voxel city data with custom simulation mesh overlays.
2225
+
2226
+ This advanced visualization function allows replacement of specific voxel classes
2227
+ with custom simulation result meshes. It's particularly useful for overlaying
2228
+ detailed simulation results (like computational fluid dynamics, thermal analysis,
2229
+ or environmental factors) onto specific building or infrastructure components.
2230
+
2231
+ The function supports simulation meshes with metadata containing numerical values
2232
+ that can be visualized using color mapping, making it ideal for displaying
2233
+ spatially-varying simulation results on building surfaces or other urban elements.
2234
+
2235
+ Parameters:
2236
+ -----------
2237
+ voxel_array : np.ndarray
2238
+ 3D array of voxel values representing the base city model.
2239
+ Shape should be (x, y, z) where each element is a voxel class ID.
2240
+
2241
+ meshsize : float
2242
+ Size of each voxel in meters, used for coordinate scaling.
2243
+
2244
+ custom_meshes : dict, optional
2245
+ Dictionary mapping voxel class IDs to custom trimesh.Trimesh objects.
2246
+ Example: {-3: building_simulation_mesh, -2: vegetation_mesh}
2247
+ These meshes will replace the standard voxel representation for visualization.
2248
+ Default is None.
2249
+
2250
+ **kwargs:
2251
+ Extensive configuration options organized by category:
2252
+
2253
+ Base Visualization:
2254
+ - vox_dict (dict): Dictionary mapping voxel class IDs to colors
2255
+ - output_directory (str): Directory for saving output files
2256
+ - output_file_name (str): Base filename for exported meshes
2257
+
2258
+ Simulation Result Display:
2259
+ - value_name (str): Name of metadata field containing simulation values
2260
+ - colormap (str): Matplotlib colormap name for simulation results
2261
+ - vmin, vmax (float): Color scale limits for simulation data
2262
+ - colorbar_title (str): Title for the simulation result colorbar
2263
+ - nan_color (str/tuple): Color for NaN/invalid simulation values, default 'gray'
2264
+
2265
+ Ground Surface Overlay:
2266
+ - sim_grid (np.ndarray): 2D array with ground-level simulation values
2267
+ - dem_grid (np.ndarray): Digital elevation model for terrain height
2268
+ - view_point_height (float): Height offset for ground simulation surface
2269
+
2270
+ Camera and Rendering:
2271
+ - projection_type (str): 'perspective' or 'orthographic', default 'perspective'
2272
+ - distance_factor (float): Camera distance multiplier, default 1.0
2273
+ - window_width, window_height (int): Render resolution
2274
+
2275
+ Output Control:
2276
+ - show_views (bool): Whether to display rendered views, default True
2277
+ - save_obj (bool): Whether to export OBJ mesh files, default False
2278
+
2279
+ Returns:
2280
+ --------
2281
+ list
2282
+ List of (view_name, image_file_path) tuples for generated views.
2283
+ Only returned if show_views=True.
2284
+
2285
+ Notes:
2286
+ ------
2287
+ Simulation Mesh Requirements:
2288
+ - Custom meshes should have simulation values stored in mesh.metadata[value_name]
2289
+ - Values can include NaN for areas without valid simulation data
2290
+ - Mesh geometry should align with the voxel grid coordinate system
2291
+
2292
+ Color Mapping:
2293
+ - Simulation values are mapped to colors using the specified colormap
2294
+ - NaN values are rendered in the specified nan_color
2295
+ - A colorbar is automatically generated and displayed
2296
+
2297
+ Technical Implementation:
2298
+ - Uses PyVista for high-quality 3D rendering
2299
+ - Supports both individual mesh coloring and ground surface overlays
2300
+ - Automatically handles coordinate system transformations
2301
+ - Generates multiple camera views for comprehensive visualization
2302
+
2303
+ Examples:
2304
+ ---------
2305
+ >>> # Basic usage with building simulation results
2306
+ >>> building_mesh = trimesh.load('building_with_cfd_results.ply')
2307
+ >>> building_mesh.metadata = {'temperature': temperature_values}
2308
+ >>> custom_meshes = {-3: building_mesh} # -3 is building class ID
2309
+ >>>
2310
+ >>> visualize_voxcity_with_sim_meshes(
2311
+ ... voxel_array, meshsize=2.0,
2312
+ ... custom_meshes=custom_meshes,
2313
+ ... value_name='temperature',
2314
+ ... colormap='plasma',
2315
+ ... colorbar_title='Temperature (°C)',
2316
+ ... vmin=15, vmax=35
2317
+ ... )
2318
+
2319
+ >>> # With ground-level wind simulation overlay
2320
+ >>> wind_mesh = create_wind_simulation_mesh(wind_data)
2321
+ >>> visualize_voxcity_with_sim_meshes(
2322
+ ... voxel_array, 2.0,
2323
+ ... custom_meshes={-3: wind_mesh},
2324
+ ... value_name='wind_speed',
2325
+ ... sim_grid=ground_wind_grid,
2326
+ ... dem_grid=elevation_grid,
2327
+ ... colormap='viridis',
2328
+ ... projection_type='orthographic',
2329
+ ... save_obj=True
2330
+ ... )
2331
+
2332
+ >>> # Multiple simulation types with custom styling
2333
+ >>> meshes = {
2334
+ ... -3: building_thermal_mesh, # Buildings with thermal data
2335
+ ... -2: vegetation_co2_mesh # Vegetation with CO2 absorption
2336
+ ... }
2337
+ >>> visualize_voxcity_with_sim_meshes(
2338
+ ... voxel_array, 2.0,
2339
+ ... custom_meshes=meshes,
2340
+ ... value_name='co2_flux',
2341
+ ... colormap='RdYlBu_r',
2342
+ ... nan_color='lightgray',
2343
+ ... distance_factor=1.5,
2344
+ ... output_file_name='co2_analysis'
2345
+ ... )
2346
+
2347
+ See Also:
2348
+ ---------
2349
+ - visualize_building_sim_results(): Specialized function for building simulations
2350
+ - visualize_voxcity_multi_view(): Basic voxel visualization without custom meshes
2351
+ - create_multi_view_scene(): Lower-level rendering function
2352
+ """
2353
+ os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2354
+ os.environ['DISPLAY'] = ':99'
2355
+
2356
+ # Configure PyVista settings
2357
+ pv.set_plot_theme('document')
2358
+ pv.global_theme.background = 'white'
2359
+ window_width = kwargs.get("window_width", 1024)
2360
+ window_height = kwargs.get("window_height", 768)
2361
+ pv.global_theme.window_size = [window_width, window_height]
2362
+ pv.global_theme.jupyter_backend = 'static'
2363
+
2364
+ # Parse kwargs
2365
+ voxel_color_map = kwargs.get("voxel_color_map", 'default')
2366
+ vox_dict = get_voxel_color_map(voxel_color_map)
2367
+ output_directory = kwargs.get("output_directory", 'output')
2368
+ base_filename = kwargs.get("output_file_name", None)
2369
+ sim_grid = kwargs.get("sim_grid", None)
2370
+ dem_grid_ori = kwargs.get("dem_grid", None)
2371
+ if dem_grid_ori is not None:
2372
+ dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2373
+ z_offset = kwargs.get("view_point_height", 1.5)
2374
+ cmap_name = kwargs.get("colormap", "viridis")
2375
+ vmin = kwargs.get("vmin", None)
2376
+ vmax = kwargs.get("vmax", None)
2377
+ projection_type = kwargs.get("projection_type", "perspective")
2378
+ distance_factor = kwargs.get("distance_factor", 1.0)
2379
+ colorbar_title = kwargs.get("colorbar_title", "")
2380
+ value_name = kwargs.get("value_name", None)
2381
+ nan_color = kwargs.get("nan_color", "gray")
2382
+ show_views = kwargs.get("show_views", True)
2383
+ save_obj = kwargs.get("save_obj", False)
2384
+
2385
+ if value_name is None:
2386
+ print("Set value_name")
2387
+
2388
+ # Create meshes from voxel data
2389
+ print("Creating voxel meshes...")
2390
+ meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2391
+
2392
+ # Replace specific voxel class meshes with custom simulation meshes
2393
+ if custom_meshes is not None:
2394
+ for class_id, custom_mesh in custom_meshes.items():
2395
+ # Apply coloring to custom meshes if they have metadata values
2396
+ if hasattr(custom_mesh, 'metadata') and value_name in custom_mesh.metadata:
2397
+ # Create a colored copy of the mesh for visualization
2398
+ import matplotlib.cm as cm
2399
+ import matplotlib.colors as mcolors
2400
+
2401
+ # Get values from metadata
2402
+ values = custom_mesh.metadata[value_name]
2403
+
2404
+ # Set vmin/vmax if not provided
2405
+ local_vmin = vmin if vmin is not None else np.nanmin(values[~np.isnan(values)])
2406
+ local_vmax = vmax if vmax is not None else np.nanmax(values[~np.isnan(values)])
2407
+
2408
+ # Create colors
2409
+ cmap = cm.get_cmap(cmap_name)
2410
+ norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
2411
+
2412
+ # Handle NaN values with custom color
2413
+ face_colors = np.zeros((len(values), 4))
2414
+
2415
+ # Convert string color to RGBA if needed
2416
+ if isinstance(nan_color, str):
2417
+ import matplotlib.colors as mcolors
2418
+ nan_rgba = np.array(mcolors.to_rgba(nan_color))
2419
+ else:
2420
+ # Assume it's already a tuple/list of RGBA values
2421
+ nan_rgba = np.array(nan_color)
2422
+
2423
+ # Apply colors: NaN values get nan_color, others get colormap colors
2424
+ nan_mask = np.isnan(values)
2425
+ face_colors[~nan_mask] = cmap(norm(values[~nan_mask]))
2426
+ face_colors[nan_mask] = nan_rgba
2427
+
2428
+ # Create a copy with colors
2429
+ vis_mesh = custom_mesh.copy()
2430
+ vis_mesh.visual.face_colors = face_colors
2431
+
2432
+ # Prepare the colormap and create colorbar
2433
+ norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
2434
+ scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2435
+
2436
+ # Create a figure and axis for the colorbar but don't display
2437
+ fig, ax = plt.subplots(figsize=(6, 1))
2438
+ cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2439
+ if colorbar_title:
2440
+ cbar.set_label(colorbar_title)
2441
+ plt.tight_layout()
2442
+ plt.show()
2443
+
2444
+ if class_id in meshes:
2445
+ print(f"Replacing voxel class {class_id} with colored custom simulation mesh")
2446
+ meshes[class_id] = vis_mesh
2447
+ else:
2448
+ print(f"Adding colored custom simulation mesh for class {class_id}")
2449
+ meshes[class_id] = vis_mesh
2450
+ else:
2451
+ # No metadata values, use the mesh as is
2452
+ if class_id in meshes:
2453
+ print(f"Replacing voxel class {class_id} with custom simulation mesh")
2454
+ meshes[class_id] = custom_mesh
2455
+ else:
2456
+ print(f"Adding custom simulation mesh for class {class_id}")
2457
+ meshes[class_id] = custom_mesh
2458
+
2459
+ # Create sim_grid surface mesh if provided
2460
+ if sim_grid is not None and dem_grid is not None:
2461
+ print("Creating sim_grid surface mesh...")
2462
+
2463
+ # If vmin/vmax not provided, use actual min/max of the valid sim data
2464
+ if vmin is None:
2465
+ vmin = np.nanmin(sim_grid)
2466
+ if vmax is None:
2467
+ vmax = np.nanmax(sim_grid)
2468
+
2469
+ sim_mesh = create_sim_surface_mesh(
2470
+ sim_grid, dem_grid,
2471
+ meshsize=meshsize,
2472
+ z_offset=z_offset,
2473
+ cmap_name=cmap_name,
2474
+ vmin=vmin,
2475
+ vmax=vmax,
2476
+ nan_color=nan_color # Pass nan_color to the mesh creation
2477
+ )
2478
+ if sim_mesh is not None:
2479
+ meshes["sim_surface"] = sim_mesh
2480
+
2481
+ # # Prepare the colormap and create colorbar
2482
+ # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2483
+ # scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2484
+
2485
+ # # Create a figure and axis for the colorbar but don't display
2486
+ # fig, ax = plt.subplots(figsize=(6, 1))
2487
+ # cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2488
+ # if colorbar_title:
2489
+ # cbar.set_label(colorbar_title)
2490
+ # plt.tight_layout()
2491
+ # plt.show()
2492
+
2493
+ # Export if filename provided
2494
+ if base_filename is not None:
2495
+ print(f"Exporting files to '{base_filename}.*' ...")
2496
+ # Create output directory if it doesn't exist
2497
+ os.makedirs(output_directory, exist_ok=True)
2498
+ export_meshes(meshes, output_directory, base_filename)
2499
+
2500
+
2501
+ # Create and save multiple views
2502
+ print("Creating multiple views...")
2503
+ # Create output directory if it doesn't exist
2504
+ os.makedirs(output_directory, exist_ok=True)
2505
+
2506
+ if show_views:
2507
+ image_files = create_multi_view_scene(meshes, output_directory=output_directory,
2508
+ projection_type=projection_type,
2509
+ distance_factor=distance_factor)
2510
+
2511
+ # Display each view separately
2512
+ for view_name, img_file in image_files:
2513
+ plt.figure(figsize=(24, 16))
2514
+ img = plt.imread(img_file)
2515
+ plt.imshow(img)
2516
+ plt.title(view_name.replace('_', ' ').title(), pad=20)
2517
+ plt.axis('off')
2518
+ plt.show()
2519
+ plt.close()
2520
+
2521
+ # After creating the meshes and before visualization
2522
+ if save_obj:
2523
+ output_directory = kwargs.get('output_directory', 'output')
2524
+ output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2525
+ obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2526
+ print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2527
+
2528
+ def visualize_building_sim_results(voxel_array, meshsize, building_sim_mesh, **kwargs):
2529
+ """
2530
+ Visualize building simulation results by replacing building meshes in the original model.
2531
+
2532
+ This is a specialized wrapper around visualize_voxcity_with_sim_meshes that specifically
2533
+ targets building simulation meshes (assuming building class ID is -3).
2534
+
2535
+ Parameters
2536
+ ----------
2537
+ voxel_array : np.ndarray
2538
+ 3D array of voxel values.
2539
+ meshsize : float
2540
+ Size of each voxel in meters.
2541
+ building_sim_mesh : trimesh.Trimesh
2542
+ Simulation result mesh for buildings with values stored in metadata.
2543
+ **kwargs:
2544
+ Same parameters as visualize_voxcity_with_sim_meshes.
2545
+ Additional parameters:
2546
+ value_name : str
2547
+ Name of the field in metadata containing values to visualize (default: 'svf_values')
2548
+ nan_color : str or tuple
2549
+ Color for NaN values (default: 'gray')
2550
+
2551
+ Returns
2552
+ -------
2553
+ list
2554
+ List of (view_name, image_file_path) tuples for the generated views.
2555
+ """
2556
+ # Building class ID is typically -3 in voxcity
2557
+ building_class_id = kwargs.get("building_class_id", -3)
2558
+
2559
+ # Create custom meshes dictionary with the building simulation mesh
2560
+ custom_meshes = {building_class_id: building_sim_mesh}
2561
+
2562
+ # Add colorbar title if not provided
2563
+ if "colorbar_title" not in kwargs:
2564
+ # Try to guess a title based on the mesh name/type
2565
+ if hasattr(building_sim_mesh, 'name') and building_sim_mesh.name:
2566
+ kwargs["colorbar_title"] = building_sim_mesh.name
2567
+ else:
2568
+ # Use value_field name as fallback
2569
+ value_name = kwargs.get("value_name", "svf_values")
2570
+ pretty_name = value_name.replace('_', ' ').title()
2571
+ kwargs["colorbar_title"] = pretty_name
2572
+
2573
+ # Call the more general visualization function
2574
+ visualize_voxcity_with_sim_meshes(
2575
+ voxel_array,
2576
+ meshsize,
2577
+ custom_meshes=custom_meshes,
2578
+ **kwargs
2579
2579
  )