Spatial Digger
Spatial Digger

Reputation: 1993

Python, placing alternating rectangles within a polygon

I'm trying to solve the following problem:

I need to place rectangles inside a polygon (must not intersect) in an alternating east-west, north-south orientation.

Variables, user defined

The area could be a uniform shape like polygon1 or irregular like polygon2, or more complex shapes like multipolygons.

import shapely.wkt
polygon1 = shapely.wkt.loads('POLYGON ((0.0 0.0, 1000.0 0.0, 1000.0 1000.0, 0.0 1000.0,  0.0 0.0))')
polygon2 = shapely.wkt.loads('POLYGON ((0.0 0.0, 1000.0 0.0, 1000.0 1000.0, 500.0 2000.0, 0.0 1000.0,  0.0 0.0))')

I have looked at the following which does similar for points Fastest way to produce a grid of points that fall within a polygon or shape?

Essentially I could adapt it then replace each point with a rectangles, but then a rectangles will overlap polygon1 or polygon2.

In the described pattern the centroid of each rectangle can be best described as being on a uniform grid. The rectangles should not overlap each other, be separated by at least the tolerance value, and orientated N-S then E-W alternating.

Any suggestions on where to begin would be greatly appreciated, I assume using geopandas and shapely.

Upvotes: 1

Views: 1007

Answers (1)

Rob Raymond
Rob Raymond

Reputation: 31166

import shapely.wkt, shapely.geometry
import geopandas as gpd
import numpy as np
import pandas as pd


def rect(polygon, n=None, size=None, tol=0, clip=True, include_poly=False):
    assert (n is None and size is not None) or (n is not None and size is None)

    a, b, c, d = gpd.GeoSeries(polygon).total_bounds
    if not n is None:
        xa = np.linspace(a, c, n + 1)
        ya = np.linspace(b, d, n + 1)
    else:
        xa = np.arange(a, c + 1, size[0])
        ya = np.arange(b, d + 1, size[1])

    # offsets for tolerance
    if tol != 0:
        tol_xa = np.arange(0, tol * len(xa), tol)
        tol_ya = np.arange(0, tol * len(ya), tol)

    else:
        tol_xa = np.zeros(len(xa))
        tol_ya = np.zeros(len(ya))

    # combine placements of x&y with tolerance
    xat = np.repeat(xa, 2)[1:] + np.repeat(tol_xa, 2)[:-1]
    yat = np.repeat(ya, 2)[1:] + np.repeat(tol_ya, 2)[:-1]

    # create a grid
    grid = gpd.GeoSeries(
        [
            shapely.geometry.box(minx, miny, maxx, maxy)
            for minx, maxx in xat[:-1].reshape(len(xa) - 1, 2)
            for miny, maxy in yat[:-1].reshape(len(ya) - 1, 2)
        ]
    )

    # make sure all returned polygons are within boundary
    if clip:
        # grid = grid.loc[grid.within(gpd.GeoSeries(np.repeat([polygon], len(grid))))]
        grid = gpd.sjoin(
            gpd.GeoDataFrame(geometry=grid),
            gpd.GeoDataFrame(geometry=[polygon]),
            how="inner",
            predicate="within",
        )["geometry"]
    # useful for visualisation
    if include_poly:
        grid = pd.concat(
            [
                grid,
                gpd.GeoSeries(
                    polygon.geoms
                    if isinstance(polygon, shapely.geometry.MultiPolygon)
                    else polygon
                ),
            ]
        )
    return grid


# let's test it...
polygon1 = shapely.wkt.loads(
    "POLYGON ((0.0 0.0, 1000.0 0.0, 1000.0 1000.0, 0.0 1000.0,  0.0 0.0))"
)
polygon2 = shapely.wkt.loads(
    "POLYGON ((0.0 0.0, 1000.0 0.0, 1000.0 1000.0, 500.0 2000.0, 0.0 1000.0,  0.0 0.0))"
)

import matplotlib.pyplot as plt
fig, ax = plt.subplots(3, 2, figsize=(16,10))

rect(polygon1, n=8, tol=0).exterior.plot(ax=ax[0,0])
rect(polygon1, n=8, tol=25).exterior.plot(ax=ax[0,1])
rect(polygon2, n=8, tol=25, include_poly=True).exterior.plot(ax=ax[1,0])
rect(polygon2, size=(35, 35), tol=25).exterior.plot(ax=ax[1,1])

# more complex polygon
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
bel_4326 = world.loc[world["iso_a3"].eq("BEL"), "geometry"].values[0]
rect(bel_4326, n=20, include_poly=True).exterior.plot(ax=ax[2,0])
# multi-polygon, use UTM so params can be defined in meters
uk = world.loc[world["iso_a3"].eq("GBR"), "geometry"]
uk = uk.to_crs(uk.estimate_utm_crs())
rect(uk.values[0], size=(3*10**4, 4*10**4), tol=10000, include_poly=True).exterior.plot(ax=ax[2,1])

sample cases visualised

enter image description here

Upvotes: 2

Related Questions