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.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {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) + 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
+ 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.index(xs_flat, ys_flat)
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
- """Create a grid of land cover classes from GeoDataFrame polygon data."""
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 shapely.geometry import box
85
- from shapely.errors import GEOSException
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, create_cell_polygon
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
- 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]
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
- 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
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.index(xs_flat, ys_flat)
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)