@@ -608,6 +608,8 @@ def shrink_countries_by_km(
608608def generate_grid_in_polygon (
609609 polygon : shp .geometry .Polygon ,
610610 hexagon_radius : float ,
611+ rotation_deg : typing .Optional [float ] = None ,
612+ translation : typing .Optional [typing .Tuple [float , float ]] = None ,
611613):
612614 """Generate a hexagonal grid inside a polygon and return points in EARTH_DEFAULT_CRS.
613615
@@ -621,6 +623,11 @@ def generate_grid_in_polygon(
621623 Polygon to fill with a grid.
622624 hexagon_radius : float
623625 Radius of the hexagons in the grid (in meters).
626+ rotation_deg : float or None, optional
627+ Rotation of the grid in degrees (default: None, no rotation).
628+ translation : tuple of float or None, optional
629+ Translation of the grid in meters (dx, dy) (default: None, no translation).
630+ The translation must not exceed the grid spacing in magnitude.
624631
625632 Returns
626633 -------
@@ -644,14 +651,18 @@ def generate_grid_in_polygon(
644651 # Transform to projection where unit is meters
645652 polygon_proj = shp .ops .transform (to_proj , polygon )
646653
647- # create a bounding box, afterwards we filter to polygon site
648- minx , miny , maxx , maxy = polygon_proj .bounds
654+ # Determine x/y spacing
649655 x_spacing = 3 * hexagon_radius
650656 y_spacing = hexagon_radius * np .sqrt (3 ) / 2
651657
652- x_vals = np .arange (minx , maxx + x_spacing , x_spacing )
658+ # Create a bounding circle, afterwards we filter to polygon site
659+ cx , cy = polygon_proj .centroid .coords [0 ]
660+ minx , miny , maxx , maxy = polygon_proj .bounds
661+ bbox_diag = np .hypot (maxx - minx , maxy - miny )
662+ bound_radius = bbox_diag / 2 + 3 * hexagon_radius
653663
654- y_vals = np .arange (miny , maxy + y_spacing , y_spacing )
664+ x_vals = np .arange (cx - bound_radius , cx + bound_radius + x_spacing , x_spacing )
665+ y_vals = np .arange (cy - bound_radius , cy + bound_radius + y_spacing , y_spacing )
655666
656667 x_vals , y_vals = np .meshgrid (x_vals , y_vals )
657668
@@ -661,6 +672,20 @@ def generate_grid_in_polygon(
661672 x_vals = x_vals .ravel ()
662673 y_vals = y_vals .ravel ()
663674
675+ if rotation_deg is not None :
676+ theta = np .deg2rad (rotation_deg )
677+ cos_t , sin_t = np .cos (theta ), np .sin (theta )
678+ x_rot = cos_t * (x_vals - cx ) - sin_t * (y_vals - cy ) + cx
679+ y_rot = sin_t * (x_vals - cx ) + cos_t * (y_vals - cy ) + cy
680+ x_vals , y_vals = x_rot , y_rot
681+
682+ if translation is not None :
683+ dx , dy = translation
684+ if abs (dx ) > x_spacing or abs (dy ) > y_spacing :
685+ raise ValueError ("generate_grid_in_polygon.translation (dx, dy) must not exceed grid spacing (x_spacing, y_spacing) in magnitude" )
686+ x_vals += dx
687+ y_vals += dy
688+
664689 # Return to EARTH_DEFAULT_CRS
665690 xt , yt = from_proj (x_vals , y_vals )
666691
@@ -676,34 +701,66 @@ def generate_grid_in_polygon(
676701
677702def generate_grid_in_multipolygon (
678703 poly : typing .Union [shp .geometry .MultiPolygon , shp .geometry .Polygon ],
679- km : float
704+ km : float ,
705+ random_transform_on_grid : bool = False ,
706+ rng : np .random .RandomState = None ,
680707) -> list [shp .geometry .MultiPolygon ]:
681- """Generate a grid in a MultiPolygon or Polygon, shrinking each by a given number of kilometers.
708+ """Generate a hexagonal grid in a MultiPolygon or Polygon,
709+ considering a hexagon radius in km.
682710
683- For each polygon, create a grid and return a single 2xN array of longitudes and latitudes.
711+ For each polygon, create a grid and return a single 2xN array of longitudes and latitudes
712+ containing all grids.
684713
685714 Parameters
686715 ----------
687716 poly : typing.Union[shp.geometry.MultiPolygon, shp.geometry.Polygon]
688717 The MultiPolygon or Polygon to process.
689718 km : float
690- Number of kilometers to shrink each polygon by.
719+ Hexagon radius in km
720+ random_transform_on_grid : bool, optional
721+ Whether to apply a random rotation and translation to the grid (default: False).
722+ rng : np.random.RandomState, optional
723+ Random number generator to use if random_transform_on_grid is True (default: None).
691724
692725 Returns
693726 -------
694727 np.ndarray
695728 2xN array: first row is longitudes, second row is latitudes.
696729 """
730+ if random_transform_on_grid :
731+ assert rng is not None
732+
697733 lons = []
698734 lats = []
699735
700736 if poly .geom_type == 'Polygon' :
701- x , y = generate_grid_in_polygon (poly , km )
737+ if random_transform_on_grid :
738+ x , y = generate_grid_in_polygon (
739+ poly , km ,
740+ rng .uniform (- 180. , 180. ),
741+ (
742+ 3 * rng .uniform (- km , km ),
743+ rng .uniform (- km , km ) * np .sqrt (3 ) / 2
744+ )
745+ )
746+ else :
747+ x , y = generate_grid_in_polygon (poly , km )
702748 lons .extend (x )
703749 lats .extend (y )
704750 elif poly .geom_type == 'MultiPolygon' :
705751 for p in poly .geoms :
706- x , y = generate_grid_in_polygon (p , km )
752+ if random_transform_on_grid :
753+ x , y = generate_grid_in_polygon (
754+ p , km ,
755+ rng .uniform (- 180. , 180. ),
756+ (
757+ 3 * rng .uniform (- km , km ),
758+ rng .uniform (- km , km ) * np .sqrt (3 ) / 2
759+ )
760+ )
761+ else :
762+ x , y = generate_grid_in_polygon (p , km )
763+
707764 lons .extend (x )
708765 lats .extend (y )
709766
0 commit comments