voxcity 0.7.0__py3-none-any.whl → 1.0.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxcity/__init__.py +14 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,8 @@ import numpy as np
|
|
|
2
2
|
from typing import Tuple, Dict, Any
|
|
3
3
|
from shapely.geometry import Polygon
|
|
4
4
|
|
|
5
|
+
from ..utils import initialize_geod, calculate_distance, normalize_to_one_meter
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def apply_operation(arr: np.ndarray, meshsize: float) -> np.ndarray:
|
|
7
9
|
"""
|
|
@@ -146,5 +148,34 @@ def create_cell_polygon(
|
|
|
146
148
|
return Polygon([bottom_left, bottom_right, top_right, top_left])
|
|
147
149
|
|
|
148
150
|
|
|
151
|
+
def compute_grid_shape(rectangle_vertices, meshsize: float) -> Tuple[int, int]:
|
|
152
|
+
"""
|
|
153
|
+
Compute the grid dimensions (rows, cols) for a given rectangle and mesh size.
|
|
154
|
+
|
|
155
|
+
This is useful when you need to know the output grid shape without
|
|
156
|
+
actually creating the grid (e.g., for pre-allocating arrays or fallback shapes).
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
rectangle_vertices: List of 4 vertices [(lon, lat), ...] defining the rectangle.
|
|
160
|
+
meshsize: Grid cell size in meters.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Tuple of (grid_size_0, grid_size_1) representing grid dimensions.
|
|
164
|
+
"""
|
|
165
|
+
geod = initialize_geod()
|
|
166
|
+
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
167
|
+
|
|
168
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
169
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
170
|
+
|
|
171
|
+
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
172
|
+
side_2 = np.array(vertex_3) - np.array(vertex_0)
|
|
173
|
+
u_vec = normalize_to_one_meter(side_1, dist_side_1)
|
|
174
|
+
v_vec = normalize_to_one_meter(side_2, dist_side_2)
|
|
175
|
+
|
|
176
|
+
grid_size, _ = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
|
|
177
|
+
return grid_size
|
|
178
|
+
|
|
179
|
+
|
|
149
180
|
|
|
150
181
|
|
|
@@ -1,93 +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
|
-
|
|
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
|
+
|
|
@@ -17,9 +17,12 @@ from .core import translate_array
|
|
|
17
17
|
def tree_height_grid_from_land_cover(land_cover_grid_ori: np.ndarray) -> np.ndarray:
|
|
18
18
|
"""
|
|
19
19
|
Convert a land cover grid to a tree height grid.
|
|
20
|
+
|
|
21
|
+
Expects 1-based land cover indices where class 5 is Tree.
|
|
20
22
|
"""
|
|
21
|
-
land_cover_grid = np.flipud(land_cover_grid_ori)
|
|
22
|
-
|
|
23
|
+
land_cover_grid = np.flipud(land_cover_grid_ori)
|
|
24
|
+
# 1-based indices: 1=Bareland, 2=Rangeland, 3=Shrub, 4=Agriculture, 5=Tree, etc.
|
|
25
|
+
tree_translation_dict = {1: 0, 2: 0, 3: 0, 4: 0, 5: 10, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0}
|
|
23
26
|
tree_height_grid = translate_array(np.flipud(land_cover_grid), tree_translation_dict).astype(int)
|
|
24
27
|
return tree_height_grid
|
|
25
28
|
|
|
@@ -55,7 +58,7 @@ def create_land_cover_grid_from_geotiff_polygon(
|
|
|
55
58
|
xs, ys = new_affine * (cols, rows)
|
|
56
59
|
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
57
60
|
|
|
58
|
-
row, col = src.
|
|
61
|
+
row, col = rasterio.transform.rowcol(src.transform, xs_flat, ys_flat)
|
|
59
62
|
row, col = np.array(row), np.array(col)
|
|
60
63
|
|
|
61
64
|
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|
|
@@ -76,14 +79,35 @@ def create_land_cover_grid_from_gdf_polygon(
|
|
|
76
79
|
meshsize: float,
|
|
77
80
|
source: str,
|
|
78
81
|
rectangle_vertices: List[Tuple[float, float]],
|
|
79
|
-
default_class: str = 'Developed space'
|
|
82
|
+
default_class: str = 'Developed space',
|
|
83
|
+
detect_ocean: bool = True,
|
|
84
|
+
land_polygon = "NOT_PROVIDED"
|
|
80
85
|
) -> np.ndarray:
|
|
81
|
-
"""
|
|
86
|
+
"""
|
|
87
|
+
Create a grid of land cover classes from GeoDataFrame polygon data.
|
|
88
|
+
|
|
89
|
+
Uses vectorized rasterization for ~100x speedup over cell-by-cell intersection.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
gdf: GeoDataFrame with land cover polygons and 'class' column
|
|
93
|
+
meshsize: Grid cell size in meters
|
|
94
|
+
source: Land cover data source name (e.g., 'OpenStreetMap')
|
|
95
|
+
rectangle_vertices: List of (lon, lat) tuples defining the area
|
|
96
|
+
default_class: Default class for cells not covered by any polygon
|
|
97
|
+
detect_ocean: If True, use OSM land polygons to detect ocean areas.
|
|
98
|
+
Areas outside land polygons will be classified as 'Water'
|
|
99
|
+
instead of the default class.
|
|
100
|
+
land_polygon: Optional pre-computed land polygon from OSM coastlines.
|
|
101
|
+
If provided (including None), this is used directly.
|
|
102
|
+
If "NOT_PROVIDED", coastlines will be queried when detect_ocean=True.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
2D numpy array of land cover class names
|
|
106
|
+
"""
|
|
82
107
|
import numpy as np
|
|
83
108
|
import geopandas as gpd
|
|
84
|
-
from
|
|
85
|
-
from shapely.
|
|
86
|
-
from rtree import index
|
|
109
|
+
from rasterio import features
|
|
110
|
+
from shapely.geometry import box, Polygon as ShapelyPolygon
|
|
87
111
|
|
|
88
112
|
class_priority = get_class_priority(source)
|
|
89
113
|
|
|
@@ -92,8 +116,9 @@ def create_land_cover_grid_from_gdf_polygon(
|
|
|
92
116
|
calculate_distance,
|
|
93
117
|
normalize_to_one_meter,
|
|
94
118
|
)
|
|
95
|
-
from .core import calculate_grid_size
|
|
119
|
+
from .core import calculate_grid_size
|
|
96
120
|
|
|
121
|
+
# Calculate grid dimensions
|
|
97
122
|
geod = initialize_geod()
|
|
98
123
|
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
99
124
|
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
@@ -104,51 +129,153 @@ def create_land_cover_grid_from_gdf_polygon(
|
|
|
104
129
|
u_vec = normalize_to_one_meter(side_1, dist_side_1)
|
|
105
130
|
v_vec = normalize_to_one_meter(side_2, dist_side_2)
|
|
106
131
|
|
|
107
|
-
origin = np.array(rectangle_vertices[0])
|
|
108
132
|
grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
|
|
133
|
+
rows, cols = grid_size
|
|
134
|
+
|
|
135
|
+
# Get bounding box for the raster
|
|
136
|
+
min_lon = min(coord[0] for coord in rectangle_vertices)
|
|
137
|
+
max_lon = max(coord[0] for coord in rectangle_vertices)
|
|
138
|
+
min_lat = min(coord[1] for coord in rectangle_vertices)
|
|
139
|
+
max_lat = max(coord[1] for coord in rectangle_vertices)
|
|
140
|
+
|
|
141
|
+
# Create affine transform (top-left origin, pixel size)
|
|
142
|
+
pixel_width = (max_lon - min_lon) / cols
|
|
143
|
+
pixel_height = (max_lat - min_lat) / rows
|
|
144
|
+
transform = Affine(pixel_width, 0, min_lon, 0, -pixel_height, max_lat)
|
|
145
|
+
|
|
146
|
+
# Build class name to priority mapping, then sort classes by priority (highest priority = lowest number = rasterize last)
|
|
147
|
+
unique_classes = gdf['class'].unique().tolist()
|
|
148
|
+
if default_class not in unique_classes:
|
|
149
|
+
unique_classes.append(default_class)
|
|
150
|
+
|
|
151
|
+
# Map class names to integer codes
|
|
152
|
+
class_to_code = {cls: i for i, cls in enumerate(unique_classes)}
|
|
153
|
+
code_to_class = {i: cls for cls, i in class_to_code.items()}
|
|
154
|
+
default_code = class_to_code[default_class]
|
|
155
|
+
|
|
156
|
+
# Initialize grid with default class code
|
|
157
|
+
grid_int = np.full((rows, cols), default_code, dtype=np.int32)
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
# Sort classes by priority (highest priority last so they overwrite lower priority)
|
|
160
|
+
# Lower priority number = higher priority = should be drawn last
|
|
161
|
+
sorted_classes = sorted(unique_classes, key=lambda c: class_priority.get(c, 999), reverse=True)
|
|
162
|
+
|
|
163
|
+
# Rasterize each class in priority order (lowest priority first, highest priority last overwrites)
|
|
164
|
+
for lc_class in sorted_classes:
|
|
165
|
+
if lc_class == default_class:
|
|
166
|
+
continue # Already filled as default
|
|
167
|
+
|
|
168
|
+
class_gdf = gdf[gdf['class'] == lc_class]
|
|
169
|
+
if class_gdf.empty:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Get all geometries for this class
|
|
173
|
+
geometries = class_gdf.geometry.tolist()
|
|
174
|
+
|
|
175
|
+
# Filter out invalid geometries and fix them
|
|
176
|
+
valid_geometries = []
|
|
177
|
+
for geom in geometries:
|
|
178
|
+
if geom is None or geom.is_empty:
|
|
179
|
+
continue
|
|
180
|
+
if not geom.is_valid:
|
|
181
|
+
geom = geom.buffer(0)
|
|
182
|
+
if geom.is_valid and not geom.is_empty:
|
|
183
|
+
valid_geometries.append(geom)
|
|
184
|
+
|
|
185
|
+
if not valid_geometries:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# Create shapes for rasterization: (geometry, value) pairs
|
|
189
|
+
class_code = class_to_code[lc_class]
|
|
190
|
+
shapes = [(geom, class_code) for geom in valid_geometries]
|
|
191
|
+
|
|
192
|
+
# Rasterize this class onto the grid (overwrites previous values)
|
|
193
|
+
try:
|
|
194
|
+
features.rasterize(
|
|
195
|
+
shapes=shapes,
|
|
196
|
+
out=grid_int,
|
|
197
|
+
transform=transform,
|
|
198
|
+
all_touched=False, # Only cells whose center is inside
|
|
199
|
+
)
|
|
200
|
+
except Exception:
|
|
201
|
+
# Fallback: try each geometry individually
|
|
202
|
+
for geom, val in shapes:
|
|
130
203
|
try:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
features.rasterize(
|
|
205
|
+
shapes=[(geom, val)],
|
|
206
|
+
out=grid_int,
|
|
207
|
+
transform=transform,
|
|
208
|
+
all_touched=False,
|
|
209
|
+
)
|
|
210
|
+
except Exception:
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Convert integer codes back to class names
|
|
214
|
+
grid = np.empty((rows, cols), dtype=object)
|
|
215
|
+
for code, cls_name in code_to_class.items():
|
|
216
|
+
grid[grid_int == code] = cls_name
|
|
217
|
+
|
|
218
|
+
# Apply ocean detection BEFORE flipping if requested
|
|
219
|
+
# This uses land polygons from OSM coastlines to classify ocean areas
|
|
220
|
+
if detect_ocean:
|
|
221
|
+
try:
|
|
222
|
+
from ...downloader.ocean import get_land_polygon_for_area, get_ocean_class_for_source
|
|
223
|
+
|
|
224
|
+
ocean_class = get_ocean_class_for_source(source)
|
|
225
|
+
|
|
226
|
+
# Use provided land_polygon or query from coastlines if not provided
|
|
227
|
+
if land_polygon == "NOT_PROVIDED":
|
|
228
|
+
land_polygon = get_land_polygon_for_area(rectangle_vertices, use_cache=False)
|
|
229
|
+
|
|
230
|
+
if land_polygon is not None:
|
|
231
|
+
# Rasterize land polygon - cells inside are land, outside are ocean
|
|
232
|
+
land_mask = np.zeros((rows, cols), dtype=np.uint8)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if land_polygon.geom_type == 'Polygon':
|
|
236
|
+
land_geometries = [(land_polygon, 1)]
|
|
237
|
+
else: # MultiPolygon
|
|
238
|
+
land_geometries = [(geom, 1) for geom in land_polygon.geoms]
|
|
239
|
+
|
|
240
|
+
features.rasterize(
|
|
241
|
+
shapes=land_geometries,
|
|
242
|
+
out=land_mask,
|
|
243
|
+
transform=transform,
|
|
244
|
+
all_touched=False
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Apply ocean class to cells that are:
|
|
248
|
+
# 1. Outside land polygon (land_mask == 0)
|
|
249
|
+
# 2. Currently classified as the default class
|
|
250
|
+
ocean_cells = (land_mask == 0) & (grid == default_class)
|
|
251
|
+
ocean_count = np.sum(ocean_cells)
|
|
252
|
+
|
|
253
|
+
if ocean_count > 0:
|
|
254
|
+
grid[ocean_cells] = ocean_class
|
|
255
|
+
pct = 100 * ocean_count / grid.size
|
|
256
|
+
print(f" Ocean detection: {ocean_count:,} cells ({pct:.1f}%) classified as '{ocean_class}'")
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
print(f" Warning: Ocean rasterization failed: {e}")
|
|
260
|
+
else:
|
|
261
|
+
# No coastlines - check if area is all ocean or all land
|
|
262
|
+
from ...downloader.ocean import check_if_area_is_ocean_via_land_features
|
|
263
|
+
is_ocean = check_if_area_is_ocean_via_land_features(rectangle_vertices)
|
|
264
|
+
if is_ocean:
|
|
265
|
+
# Convert all default class cells to water
|
|
266
|
+
ocean_cells = (grid == default_class)
|
|
267
|
+
ocean_count = np.sum(ocean_cells)
|
|
268
|
+
if ocean_count > 0:
|
|
269
|
+
grid[ocean_cells] = ocean_class
|
|
270
|
+
pct = 100 * ocean_count / grid.size
|
|
271
|
+
print(f" Ocean detection: {ocean_count:,} cells ({pct:.1f}%) classified as '{ocean_class}' (open ocean)")
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
print(f" Warning: Ocean detection failed: {e}")
|
|
275
|
+
|
|
276
|
+
# Flip to match expected orientation (north-up)
|
|
277
|
+
grid = np.flipud(grid)
|
|
278
|
+
|
|
152
279
|
return grid
|
|
153
280
|
|
|
154
281
|
|
|
@@ -38,7 +38,7 @@ def create_height_grid_from_geotiff_polygon(
|
|
|
38
38
|
xs, ys = new_affine * (cols, rows)
|
|
39
39
|
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
40
40
|
|
|
41
|
-
row, col = src.
|
|
41
|
+
row, col = rasterio.transform.rowcol(src.transform, xs_flat, ys_flat)
|
|
42
42
|
row, col = np.array(row), np.array(col)
|
|
43
43
|
|
|
44
44
|
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|