voxcity 0.6.15__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. voxcity/__init__.py +14 -8
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/citygml.py +32 -18
  4. voxcity/downloader/gba.py +210 -0
  5. voxcity/downloader/gee.py +5 -1
  6. voxcity/downloader/mbfp.py +1 -1
  7. voxcity/downloader/oemj.py +80 -8
  8. voxcity/downloader/osm.py +23 -7
  9. voxcity/downloader/overture.py +26 -1
  10. voxcity/downloader/utils.py +73 -73
  11. voxcity/errors.py +30 -0
  12. voxcity/exporter/__init__.py +13 -4
  13. voxcity/exporter/cityles.py +633 -535
  14. voxcity/exporter/envimet.py +728 -708
  15. voxcity/exporter/magicavoxel.py +334 -297
  16. voxcity/exporter/netcdf.py +238 -0
  17. voxcity/exporter/obj.py +1481 -655
  18. voxcity/generator/__init__.py +44 -0
  19. voxcity/generator/api.py +675 -0
  20. voxcity/generator/grids.py +379 -0
  21. voxcity/generator/io.py +94 -0
  22. voxcity/generator/pipeline.py +282 -0
  23. voxcity/generator/voxelizer.py +380 -0
  24. voxcity/geoprocessor/__init__.py +75 -6
  25. voxcity/geoprocessor/conversion.py +153 -0
  26. voxcity/geoprocessor/draw.py +62 -12
  27. voxcity/geoprocessor/heights.py +199 -0
  28. voxcity/geoprocessor/io.py +101 -0
  29. voxcity/geoprocessor/merge_utils.py +91 -0
  30. voxcity/geoprocessor/mesh.py +806 -790
  31. voxcity/geoprocessor/network.py +708 -679
  32. voxcity/geoprocessor/overlap.py +84 -0
  33. voxcity/geoprocessor/raster/__init__.py +82 -0
  34. voxcity/geoprocessor/raster/buildings.py +428 -0
  35. voxcity/geoprocessor/raster/canopy.py +258 -0
  36. voxcity/geoprocessor/raster/core.py +150 -0
  37. voxcity/geoprocessor/raster/export.py +93 -0
  38. voxcity/geoprocessor/raster/landcover.py +156 -0
  39. voxcity/geoprocessor/raster/raster.py +110 -0
  40. voxcity/geoprocessor/selection.py +85 -0
  41. voxcity/geoprocessor/utils.py +18 -14
  42. voxcity/models.py +113 -0
  43. voxcity/simulator/common/__init__.py +22 -0
  44. voxcity/simulator/common/geometry.py +98 -0
  45. voxcity/simulator/common/raytracing.py +450 -0
  46. voxcity/simulator/solar/__init__.py +43 -0
  47. voxcity/simulator/solar/integration.py +336 -0
  48. voxcity/simulator/solar/kernels.py +62 -0
  49. voxcity/simulator/solar/radiation.py +648 -0
  50. voxcity/simulator/solar/temporal.py +434 -0
  51. voxcity/simulator/view.py +36 -2286
  52. voxcity/simulator/visibility/__init__.py +29 -0
  53. voxcity/simulator/visibility/landmark.py +392 -0
  54. voxcity/simulator/visibility/view.py +508 -0
  55. voxcity/utils/logging.py +61 -0
  56. voxcity/utils/orientation.py +51 -0
  57. voxcity/utils/weather/__init__.py +26 -0
  58. voxcity/utils/weather/epw.py +146 -0
  59. voxcity/utils/weather/files.py +36 -0
  60. voxcity/utils/weather/onebuilding.py +486 -0
  61. voxcity/visualizer/__init__.py +24 -0
  62. voxcity/visualizer/builder.py +43 -0
  63. voxcity/visualizer/grids.py +141 -0
  64. voxcity/visualizer/maps.py +187 -0
  65. voxcity/visualizer/palette.py +228 -0
  66. voxcity/visualizer/renderer.py +928 -0
  67. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/METADATA +113 -36
  68. voxcity-0.7.0.dist-info/RECORD +77 -0
  69. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/WHEEL +1 -1
  70. voxcity/generator.py +0 -1137
  71. voxcity/geoprocessor/grid.py +0 -1568
  72. voxcity/geoprocessor/polygon.py +0 -1344
  73. voxcity/simulator/solar.py +0 -2329
  74. voxcity/utils/visualization.py +0 -2660
  75. voxcity/utils/weather.py +0 -817
  76. voxcity-0.6.15.dist-info/RECORD +0 -37
  77. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/AUTHORS.rst +0 -0
  78. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,258 @@
1
+ import warnings
2
+ from typing import List, Tuple
3
+
4
+ import numpy as np
5
+ import geopandas as gpd
6
+ from shapely.geometry import Polygon, Point
7
+ from pyproj import CRS, Transformer, Geod
8
+
9
+ from ..utils import initialize_geod, calculate_distance, normalize_to_one_meter
10
+ from .core import calculate_grid_size
11
+
12
+
13
+ def create_vegetation_height_grid_from_gdf_polygon(veg_gdf, mesh_size, polygon):
14
+ """
15
+ Create a vegetation height grid from a GeoDataFrame of vegetation polygons/objects.
16
+ Cells with vegetation take the max height of intersecting features.
17
+ Returns north-up grid (row 0 = north).
18
+ """
19
+ if veg_gdf.crs is None:
20
+ warnings.warn("veg_gdf has no CRS. Assuming EPSG:4326. ")
21
+ veg_gdf = veg_gdf.set_crs(epsg=4326)
22
+ else:
23
+ if veg_gdf.crs.to_epsg() != 4326:
24
+ veg_gdf = veg_gdf.to_crs(epsg=4326)
25
+
26
+ if 'height' not in veg_gdf.columns:
27
+ raise ValueError("Vegetation GeoDataFrame must have a 'height' column.")
28
+
29
+ if isinstance(polygon, list):
30
+ poly = Polygon(polygon)
31
+ elif isinstance(polygon, Polygon):
32
+ poly = polygon
33
+ else:
34
+ raise ValueError("polygon must be a list of (lon, lat) or a shapely Polygon.")
35
+
36
+ left, bottom, right, top = poly.bounds
37
+ geod = Geod(ellps="WGS84")
38
+ _, _, width_m = geod.inv(left, bottom, right, bottom)
39
+ _, _, height_m = geod.inv(left, bottom, left, top)
40
+
41
+ num_cells_x = int(width_m / mesh_size + 0.5)
42
+ num_cells_y = int(height_m / mesh_size + 0.5)
43
+
44
+ if num_cells_x < 1 or num_cells_y < 1:
45
+ warnings.warn("Polygon bounding box is smaller than mesh_size; returning empty array.")
46
+ return np.array([])
47
+
48
+ xs = np.linspace(left, right, num_cells_x)
49
+ ys = np.linspace(top, bottom, num_cells_y)
50
+ X, Y = np.meshgrid(xs, ys)
51
+ xs_flat = X.ravel()
52
+ ys_flat = Y.ravel()
53
+
54
+ grid_points = gpd.GeoDataFrame(
55
+ geometry=[Point(lon, lat) for lon, lat in zip(xs_flat, ys_flat)],
56
+ crs="EPSG:4326"
57
+ )
58
+
59
+ joined = gpd.sjoin(
60
+ grid_points,
61
+ veg_gdf[['height', 'geometry']],
62
+ how='left',
63
+ predicate='intersects'
64
+ )
65
+
66
+ joined_agg = (
67
+ joined.groupby(joined.index).agg({'height': 'max'})
68
+ )
69
+
70
+ veg_grid = np.zeros((num_cells_y, num_cells_x), dtype=float)
71
+ for i, row_data in joined_agg.iterrows():
72
+ if not np.isnan(row_data['height']):
73
+ row_idx = i // num_cells_x
74
+ col_idx = i % num_cells_x
75
+ veg_grid[row_idx, col_idx] = row_data['height']
76
+
77
+ return np.flipud(veg_grid)
78
+
79
+
80
+ def create_dem_grid_from_gdf_polygon(terrain_gdf, mesh_size, polygon):
81
+ """
82
+ Create a height grid from a terrain GeoDataFrame using nearest-neighbor sampling.
83
+ Returns north-up grid.
84
+ """
85
+ if terrain_gdf.crs is None:
86
+ warnings.warn("terrain_gdf has no CRS. Assuming EPSG:4326. ")
87
+ terrain_gdf = terrain_gdf.set_crs(epsg=4326)
88
+ else:
89
+ if terrain_gdf.crs.to_epsg() != 4326:
90
+ terrain_gdf = terrain_gdf.to_crs(epsg=4326)
91
+
92
+ if isinstance(polygon, list):
93
+ poly = Polygon(polygon)
94
+ elif isinstance(polygon, Polygon):
95
+ poly = polygon
96
+ else:
97
+ raise ValueError("`polygon` must be a list of (lon, lat) or a shapely Polygon.")
98
+
99
+ left, bottom, right, top = poly.bounds
100
+ geod = Geod(ellps="WGS84")
101
+ _, _, width_m = geod.inv(left, bottom, right, bottom)
102
+ _, _, height_m = geod.inv(left, bottom, left, top)
103
+ num_cells_x = int(width_m / mesh_size + 0.5)
104
+ num_cells_y = int(height_m / mesh_size + 0.5)
105
+ if num_cells_x < 1 or num_cells_y < 1:
106
+ warnings.warn("Polygon bounding box is smaller than mesh_size; returning empty array.")
107
+ return np.array([])
108
+
109
+ xs = np.linspace(left, right, num_cells_x)
110
+ ys = np.linspace(top, bottom, num_cells_y)
111
+ X, Y = np.meshgrid(xs, ys)
112
+ xs_flat = X.ravel()
113
+ ys_flat = Y.ravel()
114
+
115
+ grid_points = gpd.GeoDataFrame(
116
+ geometry=[Point(lon, lat) for lon, lat in zip(xs_flat, ys_flat)],
117
+ crs="EPSG:4326"
118
+ )
119
+
120
+ if 'elevation' not in terrain_gdf.columns:
121
+ raise ValueError("terrain_gdf must have an 'elevation' column.")
122
+
123
+ try:
124
+ centroid = poly.centroid
125
+ lon_c, lat_c = float(centroid.x), float(centroid.y)
126
+ zone = int((lon_c + 180.0) // 6) + 1
127
+ epsg_proj = 32600 + zone if lat_c >= 0 else 32700 + zone
128
+ terrain_proj = terrain_gdf.to_crs(epsg=epsg_proj)
129
+ grid_points_proj = grid_points.to_crs(epsg=epsg_proj)
130
+
131
+ grid_points_elev = gpd.sjoin_nearest(
132
+ grid_points_proj,
133
+ terrain_proj[['elevation', 'geometry']],
134
+ how="left",
135
+ distance_col="dist_to_terrain"
136
+ )
137
+ grid_points_elev.index = grid_points.index
138
+ except Exception:
139
+ grid_points_elev = gpd.sjoin_nearest(
140
+ grid_points,
141
+ terrain_gdf[['elevation', 'geometry']],
142
+ how="left",
143
+ distance_col="dist_to_terrain"
144
+ )
145
+
146
+ dem_grid = np.full((num_cells_y, num_cells_x), np.nan, dtype=float)
147
+ for i, elevation_val in zip(grid_points_elev.index, grid_points_elev['elevation']):
148
+ row = i // num_cells_x
149
+ col = i % num_cells_x
150
+ dem_grid[row, col] = elevation_val
151
+ return np.flipud(dem_grid)
152
+
153
+
154
+ def create_canopy_grids_from_tree_gdf(tree_gdf, meshsize, rectangle_vertices):
155
+ """
156
+ Create canopy top and bottom height grids from a tree GeoDataFrame.
157
+ Returns (canopy_height_grid, canopy_bottom_height_grid).
158
+ """
159
+ if tree_gdf is None or len(tree_gdf) == 0:
160
+ return np.array([]), np.array([])
161
+
162
+ required_cols = ['top_height', 'bottom_height', 'crown_diameter', 'geometry']
163
+ for col in required_cols:
164
+ if col not in tree_gdf.columns:
165
+ raise ValueError(f"tree_gdf must contain '{col}' column.")
166
+
167
+ if tree_gdf.crs is None:
168
+ warnings.warn("tree_gdf has no CRS. Assuming EPSG:4326.")
169
+ tree_gdf = tree_gdf.set_crs(epsg=4326)
170
+ elif tree_gdf.crs.to_epsg() != 4326:
171
+ tree_gdf = tree_gdf.to_crs(epsg=4326)
172
+
173
+ geod = initialize_geod()
174
+ vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
175
+
176
+ dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
177
+ dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
178
+
179
+ side_1 = np.array(vertex_1) - np.array(vertex_0)
180
+ side_2 = np.array(vertex_3) - np.array(vertex_0)
181
+ u_vec = normalize_to_one_meter(side_1, dist_side_1)
182
+ v_vec = normalize_to_one_meter(side_2, dist_side_2)
183
+
184
+ origin = np.array(rectangle_vertices[0])
185
+ grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
186
+ nx, ny = grid_size[0], grid_size[1]
187
+
188
+ i_centers_m = (np.arange(nx) + 0.5) * adjusted_meshsize[0]
189
+ j_centers_m = (np.arange(ny) + 0.5) * adjusted_meshsize[1]
190
+
191
+ canopy_top = np.zeros((nx, ny), dtype=float)
192
+ canopy_bottom = np.zeros((nx, ny), dtype=float)
193
+
194
+ transform_mat = np.column_stack((u_vec, v_vec))
195
+ try:
196
+ transform_inv = np.linalg.inv(transform_mat)
197
+ except np.linalg.LinAlgError:
198
+ transform_inv = np.linalg.pinv(transform_mat)
199
+
200
+ for _, row in tree_gdf.iterrows():
201
+ geom = row['geometry']
202
+ if geom is None or not hasattr(geom, 'x'):
203
+ continue
204
+ top_h = float(row.get('top_height', 0.0) or 0.0)
205
+ bot_h = float(row.get('bottom_height', 0.0) or 0.0)
206
+ dia = float(row.get('crown_diameter', 0.0) or 0.0)
207
+ if dia <= 0 or top_h <= 0:
208
+ continue
209
+ if bot_h < 0:
210
+ bot_h = 0.0
211
+ if bot_h > top_h:
212
+ top_h, bot_h = bot_h, top_h
213
+ R = dia / 2.0
214
+ a = max((top_h - bot_h) / 2.0, 0.0)
215
+ z0 = (top_h + bot_h) / 2.0
216
+ tree_lon = float(geom.x)
217
+ tree_lat = float(geom.y)
218
+ delta = np.array([tree_lon, tree_lat]) - origin
219
+ alpha_beta = transform_inv @ delta
220
+ alpha_m = alpha_beta[0]
221
+ beta_m = alpha_beta[1]
222
+ du_cells = int(R / adjusted_meshsize[0] + 2)
223
+ dv_cells = int(R / adjusted_meshsize[1] + 2)
224
+ i_center_idx = int(alpha_m / adjusted_meshsize[0])
225
+ j_center_idx = int(beta_m / adjusted_meshsize[1])
226
+ i_min = max(0, i_center_idx - du_cells)
227
+ i_max = min(nx - 1, i_center_idx + du_cells)
228
+ j_min = max(0, j_center_idx - dv_cells)
229
+ j_max = min(ny - 1, j_center_idx + dv_cells)
230
+ if i_min > i_max or j_min > j_max:
231
+ continue
232
+ ic = i_centers_m[i_min:i_max + 1][:, None]
233
+ jc = j_centers_m[j_min:j_max + 1][None, :]
234
+ di = ic - alpha_m
235
+ dj = jc - beta_m
236
+ r = np.sqrt(di * di + dj * dj)
237
+ within = r <= R
238
+ if not np.any(within):
239
+ continue
240
+ ratio = np.clip(r / max(R, 1e-9), 0.0, 1.0)
241
+ factor = np.sqrt(1.0 - ratio * ratio)
242
+ local_top = z0 + a * factor
243
+ local_bot = z0 - a * factor
244
+ local_top_masked = np.where(within, local_top, 0.0)
245
+ local_bot_masked = np.where(within, local_bot, 0.0)
246
+ canopy_top[i_min:i_max + 1, j_min:j_max + 1] = np.maximum(
247
+ canopy_top[i_min:i_max + 1, j_min:j_max + 1], local_top_masked
248
+ )
249
+ canopy_bottom[i_min:i_max + 1, j_min:j_max + 1] = np.maximum(
250
+ canopy_bottom[i_min:i_max + 1, j_min:j_max + 1], local_bot_masked
251
+ )
252
+
253
+ canopy_bottom = np.minimum(canopy_bottom, canopy_top)
254
+ return canopy_top, canopy_bottom
255
+
256
+
257
+
258
+
@@ -0,0 +1,150 @@
1
+ import numpy as np
2
+ from typing import Tuple, Dict, Any
3
+ from shapely.geometry import Polygon
4
+
5
+
6
+ def apply_operation(arr: np.ndarray, meshsize: float) -> np.ndarray:
7
+ """
8
+ Applies a sequence of operations to an array based on a mesh size to normalize and discretize values.
9
+
10
+ 1) Divide by meshsize, 2) +0.5, 3) floor, 4) rescale by meshsize
11
+ """
12
+ step1 = arr / meshsize
13
+ step2 = step1 + 0.5
14
+ step3 = np.floor(step2)
15
+ return step3 * meshsize
16
+
17
+
18
+ def translate_array(input_array: np.ndarray, translation_dict: Dict[Any, Any]) -> np.ndarray:
19
+ """
20
+ Translate values in an array using a dictionary mapping (vectorized).
21
+
22
+ Any value not found in the mapping is replaced with an empty string.
23
+ Returns an object-dtype ndarray preserving the input shape.
24
+ """
25
+ if not isinstance(input_array, np.ndarray):
26
+ input_array = np.asarray(input_array)
27
+
28
+ getter = np.vectorize(lambda v: translation_dict.get(v, ''), otypes=[object])
29
+ return getter(input_array)
30
+
31
+
32
+ def group_and_label_cells(array: np.ndarray) -> np.ndarray:
33
+ """
34
+ Convert non-zero numbers in a 2D numpy array to sequential IDs starting from 1.
35
+ """
36
+ result = array.copy()
37
+ unique_values = sorted(set(array.flatten()) - {0})
38
+ value_to_id = {value: idx + 1 for idx, value in enumerate(unique_values)}
39
+ for value in unique_values:
40
+ result[array == value] = value_to_id[value]
41
+ return result
42
+
43
+
44
+ def process_grid_optimized(grid_bi: np.ndarray, dem_grid: np.ndarray) -> np.ndarray:
45
+ """
46
+ Optimized version that computes per-building averages without allocating huge arrays
47
+ when building IDs are large and sparse.
48
+ """
49
+ result = dem_grid.copy()
50
+ if np.any(grid_bi != 0):
51
+ if grid_bi.dtype.kind == 'f':
52
+ grid_bi_int = np.nan_to_num(grid_bi, nan=0).astype(np.int64)
53
+ else:
54
+ grid_bi_int = grid_bi.astype(np.int64)
55
+
56
+ flat_ids = grid_bi_int.ravel()
57
+ flat_dem = dem_grid.ravel()
58
+ nz_mask = flat_ids != 0
59
+ if np.any(nz_mask):
60
+ ids_nz = flat_ids[nz_mask]
61
+ vals_nz = flat_dem[nz_mask]
62
+ unique_ids, inverse_idx = np.unique(ids_nz, return_inverse=True)
63
+ sums = np.bincount(inverse_idx, weights=vals_nz)
64
+ counts = np.bincount(inverse_idx)
65
+ counts[counts == 0] = 1
66
+ means = sums / counts
67
+ result.ravel()[nz_mask] = means[inverse_idx]
68
+ return result - np.min(result)
69
+
70
+
71
+ def process_grid(grid_bi: np.ndarray, dem_grid: np.ndarray) -> np.ndarray:
72
+ """
73
+ Safe version that tries optimization first, then falls back to original method.
74
+ """
75
+ try:
76
+ return process_grid_optimized(grid_bi, dem_grid)
77
+ except Exception as e:
78
+ print(f"Optimized process_grid failed: {e}, using original method")
79
+ unique_ids = np.unique(grid_bi[grid_bi != 0])
80
+ result = dem_grid.copy()
81
+ for id_num in unique_ids:
82
+ mask = (grid_bi == id_num)
83
+ avg_value = np.mean(dem_grid[mask])
84
+ result[mask] = avg_value
85
+ return result - np.min(result)
86
+
87
+
88
+ def calculate_grid_size(
89
+ side_1: np.ndarray,
90
+ side_2: np.ndarray,
91
+ u_vec: np.ndarray,
92
+ v_vec: np.ndarray,
93
+ meshsize: float
94
+ ) -> Tuple[Tuple[int, int], Tuple[float, float]]:
95
+ """
96
+ Calculate grid size and adjusted mesh size based on input parameters.
97
+ Returns ((nx, ny), (dx, dy))
98
+ """
99
+ dist_side_1_m = np.linalg.norm(side_1) / (np.linalg.norm(u_vec) + 1e-12)
100
+ dist_side_2_m = np.linalg.norm(side_2) / (np.linalg.norm(v_vec) + 1e-12)
101
+
102
+ grid_size_0 = max(1, int(dist_side_1_m / meshsize + 0.5))
103
+ grid_size_1 = max(1, int(dist_side_2_m / meshsize + 0.5))
104
+
105
+ adjusted_mesh_size_0 = dist_side_1_m / grid_size_0
106
+ adjusted_mesh_size_1 = dist_side_2_m / grid_size_1
107
+
108
+ return (grid_size_0, grid_size_1), (adjusted_mesh_size_0, adjusted_mesh_size_1)
109
+
110
+
111
+ def create_coordinate_mesh(
112
+ origin: np.ndarray,
113
+ grid_size: Tuple[int, int],
114
+ adjusted_meshsize: Tuple[float, float],
115
+ u_vec: np.ndarray,
116
+ v_vec: np.ndarray
117
+ ) -> np.ndarray:
118
+ """
119
+ Create a coordinate mesh based on input parameters.
120
+ Returns array of shape (coord_dim, ny, nx)
121
+ """
122
+ x = np.linspace(0, grid_size[0], grid_size[0])
123
+ y = np.linspace(0, grid_size[1], grid_size[1])
124
+ xx, yy = np.meshgrid(x, y)
125
+ cell_coords = origin[:, np.newaxis, np.newaxis] + \
126
+ xx[np.newaxis, :, :] * adjusted_meshsize[0] * u_vec[:, np.newaxis, np.newaxis] + \
127
+ yy[np.newaxis, :, :] * adjusted_meshsize[1] * v_vec[:, np.newaxis, np.newaxis]
128
+ return cell_coords
129
+
130
+
131
+ def create_cell_polygon(
132
+ origin: np.ndarray,
133
+ i: int,
134
+ j: int,
135
+ adjusted_meshsize: Tuple[float, float],
136
+ u_vec: np.ndarray,
137
+ v_vec: np.ndarray
138
+ ):
139
+ """
140
+ Create a polygon representing a grid cell.
141
+ """
142
+ bottom_left = origin + i * adjusted_meshsize[0] * u_vec + j * adjusted_meshsize[1] * v_vec
143
+ bottom_right = origin + (i + 1) * adjusted_meshsize[0] * u_vec + j * adjusted_meshsize[1] * v_vec
144
+ top_right = origin + (i + 1) * adjusted_meshsize[0] * u_vec + (j + 1) * adjusted_meshsize[1] * v_vec
145
+ top_left = origin + i * adjusted_meshsize[0] * u_vec + (j + 1) * adjusted_meshsize[1] * v_vec
146
+ return Polygon([bottom_left, bottom_right, top_right, top_left])
147
+
148
+
149
+
150
+
@@ -0,0 +1,93 @@
1
+ import numpy as np
2
+ import geopandas as gpd
3
+ from shapely.geometry import box
4
+ from pyproj import CRS, Transformer
5
+ from ...utils.orientation import ensure_orientation, ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP
6
+
7
+
8
+ def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
9
+ """
10
+ Converts a 2D grid to a GeoDataFrame with cell polygons and values.
11
+ Output CRS: EPSG:4326
12
+ """
13
+ grid = ensure_orientation(grid_ori.copy(), ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP)
14
+
15
+ min_lon = min(v[0] for v in rectangle_vertices)
16
+ max_lon = max(v[0] for v in rectangle_vertices)
17
+ min_lat = min(v[1] for v in rectangle_vertices)
18
+ max_lat = max(v[1] for v in rectangle_vertices)
19
+
20
+ rows, cols = grid.shape
21
+
22
+ wgs84 = CRS.from_epsg(4326)
23
+ web_mercator = CRS.from_epsg(3857)
24
+ transformer_to_mercator = Transformer.from_crs(wgs84, web_mercator, always_xy=True)
25
+ transformer_to_wgs84 = Transformer.from_crs(web_mercator, wgs84, always_xy=True)
26
+
27
+ min_x, min_y = transformer_to_mercator.transform(min_lon, min_lat)
28
+ max_x, max_y = transformer_to_mercator.transform(max_lon, max_lat)
29
+
30
+ cell_size_x = (max_x - min_x) / cols
31
+ cell_size_y = (max_y - min_y) / rows
32
+
33
+ polygons = []
34
+ values = []
35
+
36
+ for i in range(rows):
37
+ for j in range(cols):
38
+ cell_min_x = min_x + j * cell_size_x
39
+ cell_max_x = min_x + (j + 1) * cell_size_x
40
+ cell_min_y = max_y - (i + 1) * cell_size_y
41
+ cell_max_y = max_y - i * cell_size_y
42
+ cell_min_lon, cell_min_lat = transformer_to_wgs84.transform(cell_min_x, cell_min_y)
43
+ cell_max_lon, cell_max_lat = transformer_to_wgs84.transform(cell_max_x, cell_max_y)
44
+ cell_poly = box(cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat)
45
+ polygons.append(cell_poly)
46
+ values.append(grid[i, j])
47
+
48
+ gdf = gpd.GeoDataFrame({'geometry': polygons, 'value': values}, crs=CRS.from_epsg(4326))
49
+ return gdf
50
+
51
+
52
+ def grid_to_point_geodataframe(grid_ori, rectangle_vertices, meshsize):
53
+ """
54
+ Converts a 2D grid to a GeoDataFrame with point geometries at cell centers and values.
55
+ Output CRS: EPSG:4326
56
+ """
57
+ import geopandas as gpd
58
+ from shapely.geometry import Point
59
+
60
+ grid = ensure_orientation(grid_ori.copy(), ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP)
61
+
62
+ min_lon = min(v[0] for v in rectangle_vertices)
63
+ max_lon = max(v[0] for v in rectangle_vertices)
64
+ min_lat = min(v[1] for v in rectangle_vertices)
65
+ max_lat = max(v[1] for v in rectangle_vertices)
66
+
67
+ rows, cols = grid.shape
68
+
69
+ wgs84 = CRS.from_epsg(4326)
70
+ web_mercator = CRS.from_epsg(3857)
71
+ transformer_to_mercator = Transformer.from_crs(wgs84, web_mercator, always_xy=True)
72
+ transformer_to_wgs84 = Transformer.from_crs(web_mercator, wgs84, always_xy=True)
73
+
74
+ min_x, min_y = transformer_to_mercator.transform(min_lon, min_lat)
75
+ max_x, max_y = transformer_to_mercator.transform(max_lon, max_lat)
76
+
77
+ cell_size_x = (max_x - min_x) / cols
78
+ cell_size_y = (max_y - min_y) / rows
79
+
80
+ points = []
81
+ values = []
82
+ for i in range(rows):
83
+ for j in range(cols):
84
+ cell_center_x = min_x + (j + 0.5) * cell_size_x
85
+ cell_center_y = max_y - (i + 0.5) * cell_size_y
86
+ center_lon, center_lat = transformer_to_wgs84.transform(cell_center_x, cell_center_y)
87
+ points.append(Point(center_lon, center_lat))
88
+ values.append(grid[i, j])
89
+
90
+ gdf = gpd.GeoDataFrame({'geometry': points, 'value': values}, crs=CRS.from_epsg(4326))
91
+ return gdf
92
+
93
+
@@ -0,0 +1,156 @@
1
+ import numpy as np
2
+ from typing import List, Tuple, Dict, Any
3
+ from shapely.geometry import Polygon
4
+ from affine import Affine
5
+ import rasterio
6
+
7
+ from pyproj import Geod
8
+
9
+ from ...utils.lc import (
10
+ get_class_priority,
11
+ create_land_cover_polygons,
12
+ get_dominant_class,
13
+ )
14
+ from .core import translate_array
15
+
16
+
17
+ def tree_height_grid_from_land_cover(land_cover_grid_ori: np.ndarray) -> np.ndarray:
18
+ """
19
+ Convert a land cover grid to a tree height grid.
20
+ """
21
+ land_cover_grid = np.flipud(land_cover_grid_ori) + 1
22
+ tree_translation_dict = {1: 0, 2: 0, 3: 0, 4: 10, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0}
23
+ tree_height_grid = translate_array(np.flipud(land_cover_grid), tree_translation_dict).astype(int)
24
+ return tree_height_grid
25
+
26
+
27
+ def create_land_cover_grid_from_geotiff_polygon(
28
+ tiff_path: str,
29
+ mesh_size: float,
30
+ land_cover_classes: Dict[str, Any],
31
+ polygon: List[Tuple[float, float]]
32
+ ) -> np.ndarray:
33
+ """
34
+ Create a land cover grid from a GeoTIFF file within a polygon boundary.
35
+ """
36
+ with rasterio.open(tiff_path) as src:
37
+ img = src.read((1, 2, 3))
38
+ left, bottom, right, top = src.bounds
39
+ poly = Polygon(polygon)
40
+ left_wgs84, bottom_wgs84, right_wgs84, top_wgs84 = poly.bounds
41
+
42
+ geod = Geod(ellps="WGS84")
43
+ _, _, width = geod.inv(left_wgs84, bottom_wgs84, right_wgs84, bottom_wgs84)
44
+ _, _, height = geod.inv(left_wgs84, bottom_wgs84, left_wgs84, top_wgs84)
45
+
46
+ num_cells_x = int(width / mesh_size + 0.5)
47
+ num_cells_y = int(height / mesh_size + 0.5)
48
+
49
+ adjusted_mesh_size_x = (right - left) / num_cells_x
50
+ adjusted_mesh_size_y = (top - bottom) / num_cells_y
51
+
52
+ new_affine = Affine(adjusted_mesh_size_x, 0, left, 0, -adjusted_mesh_size_y, top)
53
+
54
+ cols, rows = np.meshgrid(np.arange(num_cells_x), np.arange(num_cells_y))
55
+ xs, ys = new_affine * (cols, rows)
56
+ xs_flat, ys_flat = xs.flatten(), ys.flatten()
57
+
58
+ row, col = src.index(xs_flat, ys_flat)
59
+ row, col = np.array(row), np.array(col)
60
+
61
+ valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
62
+ row, col = row[valid], col[valid]
63
+
64
+ grid = np.full((num_cells_y, num_cells_x), 'No Data', dtype=object)
65
+ for i, (r, c) in enumerate(zip(row, col)):
66
+ cell_data = img[:, r, c]
67
+ dominant_class = get_dominant_class(cell_data, land_cover_classes)
68
+ grid_row, grid_col = np.unravel_index(i, (num_cells_y, num_cells_x))
69
+ grid[grid_row, grid_col] = dominant_class
70
+
71
+ return np.flipud(grid)
72
+
73
+
74
+ def create_land_cover_grid_from_gdf_polygon(
75
+ gdf,
76
+ meshsize: float,
77
+ source: str,
78
+ rectangle_vertices: List[Tuple[float, float]],
79
+ default_class: str = 'Developed space'
80
+ ) -> np.ndarray:
81
+ """Create a grid of land cover classes from GeoDataFrame polygon data."""
82
+ import numpy as np
83
+ import geopandas as gpd
84
+ from shapely.geometry import box
85
+ from shapely.errors import GEOSException
86
+ from rtree import index
87
+
88
+ class_priority = get_class_priority(source)
89
+
90
+ from ..utils import (
91
+ initialize_geod,
92
+ calculate_distance,
93
+ normalize_to_one_meter,
94
+ )
95
+ from .core import calculate_grid_size, create_cell_polygon
96
+
97
+ geod = initialize_geod()
98
+ vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
99
+ dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
100
+ dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
101
+
102
+ side_1 = np.array(vertex_1) - np.array(vertex_0)
103
+ side_2 = np.array(vertex_3) - np.array(vertex_0)
104
+ u_vec = normalize_to_one_meter(side_1, dist_side_1)
105
+ v_vec = normalize_to_one_meter(side_2, dist_side_2)
106
+
107
+ origin = np.array(rectangle_vertices[0])
108
+ grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
109
+
110
+ grid = np.full(grid_size, default_class, dtype=object)
111
+
112
+ extent = [min(coord[1] for coord in rectangle_vertices), max(coord[1] for coord in rectangle_vertices),
113
+ min(coord[0] for coord in rectangle_vertices), max(coord[0] for coord in rectangle_vertices)]
114
+ plotting_box = box(extent[2], extent[0], extent[3], extent[1])
115
+
116
+ land_cover_polygons = []
117
+ idx = index.Index()
118
+ for i, row in gdf.iterrows():
119
+ polygon = row.geometry
120
+ land_cover_class = row['class']
121
+ land_cover_polygons.append((polygon, land_cover_class))
122
+ idx.insert(i, polygon.bounds)
123
+
124
+ for i in range(grid_size[0]):
125
+ for j in range(grid_size[1]):
126
+ land_cover_class = default_class
127
+ cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
128
+ for k in idx.intersection(cell.bounds):
129
+ polygon, land_cover_class_temp = land_cover_polygons[k]
130
+ try:
131
+ if cell.intersects(polygon):
132
+ intersection = cell.intersection(polygon)
133
+ if intersection.area > cell.area / 2:
134
+ rank = class_priority[land_cover_class]
135
+ rank_temp = class_priority[land_cover_class_temp]
136
+ if rank_temp < rank:
137
+ land_cover_class = land_cover_class_temp
138
+ grid[i, j] = land_cover_class
139
+ except GEOSException as e:
140
+ try:
141
+ fixed_polygon = polygon.buffer(0)
142
+ if cell.intersects(fixed_polygon):
143
+ intersection = cell.intersection(fixed_polygon)
144
+ if intersection.area > cell.area / 2:
145
+ rank = class_priority[land_cover_class]
146
+ rank_temp = class_priority[land_cover_class_temp]
147
+ if rank_temp < rank:
148
+ land_cover_class = land_cover_class_temp
149
+ grid[i, j] = land_cover_class
150
+ except Exception:
151
+ continue
152
+ return grid
153
+
154
+
155
+
156
+