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