Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 83 additions & 36 deletions momepy/functional/_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
GPD_GE_013 = Version(gpd.__version__) >= Version("0.13.0")
GPD_GE_10 = Version(gpd.__version__) >= Version("1.0dev")
LPS_GE_411 = Version(libpysal.__version__) >= Version("4.11.dev")
SPL_GE_210 = Version(shapely.__version__) >= Version("2.1.0rc1")

__all__ = [
"morphological_tessellation",
Expand Down Expand Up @@ -137,6 +138,7 @@
cell_size: float = 1.0,
neighbor_mode: str = "moore",
barriers_for_inner: GeoSeries | GeoDataFrame = None,
cell_tolerance: float = 2.0
) -> GeoDataFrame:
"""Generate enclosed tessellation

Expand Down Expand Up @@ -283,6 +285,7 @@
cell_size,
neighbor_mode,
barriers_for_inner,
cell_tolerance
)
for t in tuples
)
Expand Down Expand Up @@ -328,6 +331,7 @@
cell_size,
neighbor_mode,
barriers_for_inner,
cell_tolerance
):
"""Generate tessellation for a single enclosure. Helper for enclosed_tessellation"""
# check if threshold is set and filter buildings based on the threshold
Expand All @@ -338,7 +342,7 @@
> (shapely.area(blg.geometry.array) * threshold)
]
else:
blg = blg[

Check warning on line 345 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L345

Added line #L345 was not covered by tests
shapely.area(
shapely.intersection(
blg.geometry.array, shapely.geometry.Polygon(poly.boundary)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the rationale behind this? Why are you ignoring holes when a MultiPolygon is provided? Feels inconsistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial intention was to let the poly be accommodated with inner barriers, but now they're separated. Somehow this part remained, so will remove it.

Expand All @@ -358,12 +362,13 @@
as_gdf=True,
)
else:
tess = _voronoi_by_ca(

Check warning on line 365 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L365

Added line #L365 was not covered by tests
seed_geoms=blg,
barrier_geoms=poly,
cell_size=cell_size,
neighbor_mode=neighbor_mode,
barriers_for_inner=barriers_for_inner,
cell_tolerance=cell_tolerance * cell_size,
)
tess[enclosure_id] = ix
return tess
Expand All @@ -388,6 +393,7 @@
cell_size: float = 1.0,
neighbor_mode: str = "moore",
barriers_for_inner: GeoSeries | GeoDataFrame = None,
cell_tolerance: float = 2.0,
) -> GeoDataFrame:
"""
Generate an aggregated Voronoi tessellation as a GeoDataFrame via a cellular automata.
Expand All @@ -409,106 +415,131 @@
barrier_geoms: GeoDataFrame containing barrier features or a shapely Polygon.
cell_size: Grid cell size. By default it is 1.0.
neighbor_mode: Choice of neighbor connectivity ('moore' or 'neumann'). By default it is 'moore'.
barriers_for_inner: GeoDataFrame containing inner barriers to be included. By default it is None.
barriers_for_inner: GeoDataFrame containing inner barriers to be included. By default it is None
cell_tolerance: Tolerance for simplifying the grid cells. By default it is 2.0.


Returns:
A GeoDataFrame representing the aggregated Voronoi tessellation, clipped by barriers.
"""

# If there is barriers_for_inner, add the intersected or contained barriers to the barrier_geoms
if barriers_for_inner is not None:

Check warning on line 427 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L427

Added line #L427 was not covered by tests
# get inner barriers
inner_barriers = _get_inner_barriers(

Check warning on line 429 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L429

Added line #L429 was not covered by tests
barrier_geoms, barriers_for_inner.to_crs(seed_geoms.crs)
)

if barrier_geoms.geom_type == "Polygon":
# Wrap a single barrier geometry (Polygon, LineString, or GeometryCollection)
barrier_geoms = GeoSeries([barrier_geoms.boundary], crs=seed_geoms.crs)
# Handle barrier_geoms if it is a Polygon or MultiPolygon
if barrier_geoms.geom_type == "Polygon":

Check warning on line 434 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L434

Added line #L434 was not covered by tests
# Take buffer of polygon and extract its exterior boundary
barrier_geoms_buffered = GeoSeries(

Check warning on line 436 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L436

Added line #L436 was not covered by tests
[barrier_geoms.buffer(10).exterior], crs=seed_geoms.crs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does 10 come from here? Can't use hardcoded values here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This amount of buffer was needed to ensure that all the extent are assigned to either of the cell state, which could be arbitrary number (This kind of vacant cells occurred between three tessellations). Will set the number based on the cell size automatically.

)
barrier_geoms = GeoSeries([barrier_geoms], crs=seed_geoms.crs)

Check warning on line 439 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L439

Added line #L439 was not covered by tests

elif barrier_geoms.geom_type == "MultiPolygon":

Check warning on line 441 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L441

Added line #L441 was not covered by tests
# Process each polygon: take buffer then exterior boundary (to ensure there's no gap between enclosures)
barrier_geoms_buffered = GeoSeries(

Check warning on line 443 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L443

Added line #L443 was not covered by tests
[poly.buffer(10).exterior for poly in barrier_geoms.geoms],
crs=seed_geoms.crs,
)
barrier_geoms = GeoSeries(barrier_geoms, crs=seed_geoms.crs)

Check warning on line 447 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L447

Added line #L447 was not covered by tests

if len(inner_barriers) > 0:
# add inner barriers as a list of geometry to the barrier_geoms
barrier_geoms = pd.concat(
[barrier_geoms, inner_barriers], ignore_index=True
)
else:
if barrier_geoms.geom_type == "Polygon":
# Wrap a single barrier geometry (Polygon, LineString, or GeometryCollection)
barrier_geoms = GeoSeries([barrier_geoms.boundary], crs=seed_geoms.crs)
raise ValueError("barrier_geoms must be a Polygon or MultiPolygon")

Check warning on line 450 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L450

Added line #L450 was not covered by tests

outer_union = shapely.ops.unary_union(barrier_geoms_buffered)

Check warning on line 452 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L452

Added line #L452 was not covered by tests

# Compute inner barriers union if available
if (

Check warning on line 455 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L455

Added line #L455 was not covered by tests
"inner_barriers" in locals()
and inner_barriers is not None
and not inner_barriers.empty
):
inner_union = shapely.ops.unary_union(inner_barriers.geometry)

Check warning on line 460 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L460

Added line #L460 was not covered by tests
else:
inner_union = None

Check warning on line 462 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L462

Added line #L462 was not covered by tests

# Combine outer barrier with inner barriers
if outer_union and inner_union:
prep_barrier = shapely.ops.unary_union([outer_union, inner_union])
elif outer_union:
prep_barrier = outer_union
elif inner_union:
prep_barrier = inner_union

Check warning on line 470 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L465-L470

Added lines #L465 - L470 were not covered by tests
else:
prep_barrier = None

Check warning on line 472 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L472

Added line #L472 was not covered by tests

# Compute grid bounds
origin, grid_width, grid_height = _get_grid_bounds(

Check warning on line 475 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L475

Added line #L475 was not covered by tests
seed_geoms, barrier_geoms, cell_size
seed_geoms, barrier_geoms_buffered, cell_size
)

# Prepare barrier geometries if provided.
barrier_union = None
prep_barrier = None
if not barrier_geoms.empty:
barrier_union = shapely.ops.unary_union(barrier_geoms.geometry)
prep_barrier = shapely.prepared.prep(barrier_union)

# Initialize grid states with UNKNOWN values.
states = np.full((grid_height, grid_width), CellState.UNKNOWN.value, dtype=int)

Check warning on line 480 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L480

Added line #L480 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know how small the int can be? To save memory.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's four types defined for CellState, which is used before the assignment. Once a cell is assigned to any of sites, a non-negative integer (id of the building) is used in the following processes. It's for readability, so you can consider it as merely an equivalent to negative integers.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is, it was hard to read and recognise them from integers (-1: unknown, -2: boundary, -3: barrier, -4: frontier)


xs, ys = np.meshgrid(np.arange(grid_width), np.arange(grid_height))
cell_polys = GeoSeries(

Check warning on line 483 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L482-L483

Added lines #L482 - L483 were not covered by tests
[
_get_cell_polygon(x, y, cell_size, origin)
for x, y in zip(xs.flatten(), ys.flatten())
]
)

# Identify barrier cells in the grid
if prep_barrier is not None:
xs, ys = np.meshgrid(np.arange(grid_width), np.arange(grid_height))
cell_polys = GeoSeries(
[
_get_cell_polygon(x, y, cell_size, origin)
for x, y in zip(xs.flatten(), ys.flatten())
]
barrier_mask = cell_polys.intersects(prep_barrier).values.reshape(

Check warning on line 492 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L491-L492

Added lines #L491 - L492 were not covered by tests
grid_height, grid_width
)
barrier_mask = cell_polys.intersects(barrier_union).values.reshape(
(grid_height, grid_width)
)
states[barrier_mask] = CellState.BARRIER.value
else:
barrier_mask = np.zeros((grid_height, grid_width), dtype=bool)
states[barrier_mask] = CellState.BARRIER.value

Check warning on line 497 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L496-L497

Added lines #L496 - L497 were not covered by tests

# Seed the grid with seed geometries.
for site_id, geom in enumerate(seed_geoms.geometry):
if not geom.is_empty:
cells = _geom_to_cells(geom, origin, cell_size, grid_width, grid_height)
valid_cells = [

Check warning on line 503 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L500-L503

Added lines #L500 - L503 were not covered by tests
(x, y) for x, y in cells if states[y, x] == CellState.UNKNOWN.value
]
if valid_cells:
indices = np.array(valid_cells)
states[indices[:, 1], indices[:, 0]] = site_id

Check warning on line 508 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L506-L508

Added lines #L506 - L508 were not covered by tests

# Initialize the BFS queue with all seeded cells’ neighbors.
queue = deque()
seed_indices = np.argwhere(states >= 0)
for y, x in seed_indices:
_enqueue_neighbors(x, y, states, grid_width, grid_height, neighbor_mode, queue)

Check warning on line 514 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L511-L514

Added lines #L511 - L514 were not covered by tests

# Process BFS to propagate seed values.
while queue:

Check warning on line 517 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L517

Added line #L517 was not covered by tests
# Dequeue the current cell and skip if it is not a frontier cell.
x_current, y_current = queue.popleft()
if states[y_current, x_current] != CellState.FRONTIER.value:
continue

Check warning on line 521 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L519-L521

Added lines #L519 - L521 were not covered by tests

# Get neighbor cells that were already assigned a seed id or still unknown (state >= 0).
# Note that boundary or barrier cells are skipped (state < 0).
neighbor_seeds = [

Check warning on line 525 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L525

Added line #L525 was not covered by tests
states[ny, nx]
for nx, ny in _get_neighbors(
x_current, y_current, grid_width, grid_height, mode=neighbor_mode
)
if states[ny, nx] >= 0
]
if not neighbor_seeds:
continue

Check warning on line 533 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L532-L533

Added lines #L532 - L533 were not covered by tests

# Assign as a boundary if multiple seed ids are found.
if len(set(neighbor_seeds)) > 1:
states[y_current, x_current] = CellState.BOUNDARY.value

Check warning on line 537 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L536-L537

Added lines #L536 - L537 were not covered by tests
# EIf not, equeue neighbor cells for further propagation.
else:
assigned_seed = set(neighbor_seeds).pop()
states[y_current, x_current] = assigned_seed
_enqueue_neighbors(

Check warning on line 542 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L540-L542

Added lines #L540 - L542 were not covered by tests
x_current,
y_current,
states,
Expand All @@ -519,20 +550,20 @@
)

# Post-process barrier and boundary cells using a voting mechanism.
states = _assign_adjacent_seed_cells(states, neighbor_mode)

Check warning on line 553 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L553

Added line #L553 was not covered by tests

# Create grid cell polygons and build a GeoDataFrame.
xs, ys = np.meshgrid(np.arange(grid_width), np.arange(grid_height))
grid_polys = [

Check warning on line 557 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L556-L557

Added lines #L556 - L557 were not covered by tests
_get_cell_polygon(x, y, cell_size, origin)
for x, y in zip(xs.flatten(), ys.flatten())
]
grid_gdf = GeoDataFrame(

Check warning on line 561 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L561

Added line #L561 was not covered by tests
{"site_id": states.flatten()}, geometry=grid_polys, crs=seed_geoms.crs
)

# Include only cells with valid seed assignments and dissolve contiguous regions.
grid_gdf = (

Check warning on line 566 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L566

Added line #L566 was not covered by tests
grid_gdf[grid_gdf["site_id"] >= 0]
.dissolve(by="site_id")
.reset_index()
Expand All @@ -540,20 +571,31 @@
)

# Clip by barriers
if barrier_geoms is not None and (not barrier_geoms.empty):

Check warning on line 574 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L574

Added line #L574 was not covered by tests
# Create a union of the barrier geometries.
barrier_union = shapely.ops.unary_union(barrier_geoms.geometry)

Check warning on line 576 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L576

Added line #L576 was not covered by tests
# If the barrier union is not a polygon (e.g., it's a MultiLineString), polygonize it.
if not isinstance(

Check warning on line 578 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L578

Added line #L578 was not covered by tests
barrier_union, (shapely.geometry.Polygon, shapely.geometry.MultiPolygon)
):
barrier_polys = list(shapely.ops.polygonize(barrier_union))
if barrier_polys:
barrier_union = shapely.ops.unary_union(barrier_polys)

Check warning on line 583 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L581-L583

Added lines #L581 - L583 were not covered by tests
# Clip each polygon in the grid using the barrier boundary.
grid_gdf["geometry"] = grid_gdf["geometry"].intersection(barrier_union)

Check warning on line 585 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L585

Added line #L585 was not covered by tests

if cell_tolerance is not None:
if SPL_GE_210:

Check warning on line 588 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L587-L588

Added lines #L587 - L588 were not covered by tests
# Simplify coverages with the parameter cell_tolerance as the area threshold of Visvalingam-Whyatt algorithm.
grid_gdf["geometry"] = shapely.coverage_simplify(

Check warning on line 590 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L590

Added line #L590 was not covered by tests
grid_gdf["geometry"].array, tolerance=cell_tolerance
)
else:
warnings.warn(

Check warning on line 594 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L594

Added line #L594 was not covered by tests
"Shapely 2.1.0 or higher is required for coverage_simplify. Skipping."
)

return grid_gdf

Check warning on line 598 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L598

Added line #L598 was not covered by tests


def _get_inner_barriers(enclosure, barriers):
Expand All @@ -569,14 +611,17 @@
and any intersecting barriers.
"""
# Find barriers intersecting or contained in the enclosure
inner_barriers = barriers[

Check warning on line 614 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L614

Added line #L614 was not covered by tests
barriers.intersects(enclosure) | barriers.contains(enclosure)
]

# Clip those segments to stay within the enclosure
inner_barriers = gpd.clip(inner_barriers, enclosure)

Check warning on line 619 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L619

Added line #L619 was not covered by tests

# Only keep the geometry which is within the enclosure
inner_barriers = inner_barriers[inner_barriers.within(enclosure)]

Check warning on line 622 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L622

Added line #L622 was not covered by tests

return GeoSeries(inner_barriers.geometry, crs=barriers.crs)

Check warning on line 624 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L624

Added line #L624 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't inner_barriers already a GeoSeries?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked it, you're right.



class CellState(Enum):
Expand All @@ -602,8 +647,8 @@
"""
Generate a grid cell polygon based on the given indices, cell size, and origin.
"""
ox, oy = origin
return shapely.geometry.Polygon(

Check warning on line 651 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L650-L651

Added lines #L650 - L651 were not covered by tests
[
(ox + x_idx * cell_size, oy + y_idx * cell_size),
(ox + (x_idx + 1) * cell_size, oy + y_idx * cell_size),
Expand All @@ -627,14 +672,14 @@
Returns:
A list of (x, y) tuples for valid neighbor indices.
"""
neighbor_dirs = {

Check warning on line 675 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L675

Added line #L675 was not covered by tests
"moore": [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)],
"neumann": [(0, -1), (-1, 0), (1, 0), (0, 1)],
}
directions = neighbor_dirs.get(mode)
if directions is None:
raise ValueError("Invalid neighbor_mode: choose 'moore' or 'neumann'")
return [

Check warning on line 682 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L679-L682

Added lines #L679 - L682 were not covered by tests
(x + dx, y + dy)
for dx, dy in directions
if 0 <= x + dx < max_x and 0 <= y + dy < max_y
Expand All @@ -646,19 +691,21 @@
barrier_geoms: GeoSeries | GeoDataFrame,
cell_size: float,
) -> Tuple[Tuple[float, float], int, int]:
"""
Compute the grid bounds required to cover both seed and barrier geometries.
"""
seed_bounds = seed_geoms.total_bounds # [xmin, ymin, xmax, ymax]
barrier_bounds = barrier_geoms.total_bounds

Check warning on line 695 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L694-L695

Added lines #L694 - L695 were not covered by tests

xmin = min(seed_bounds[0], barrier_bounds[0])
ymin = min(seed_bounds[1], barrier_bounds[1])
xmax = max(seed_bounds[2], barrier_bounds[2])
ymax = max(seed_bounds[3], barrier_bounds[3])

Check warning on line 700 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L697-L700

Added lines #L697 - L700 were not covered by tests
grid_width = math.ceil((xmax - xmin) / cell_size)
grid_height = math.ceil((ymax - ymin) / cell_size)
return (xmin, ymin), grid_width, grid_height
# expand bounds by 1 cell in each direction
new_xmin = xmin - cell_size
new_ymin = ymin - cell_size
new_xmax = xmax + cell_size
new_ymax = ymax + cell_size
grid_width = math.ceil((new_xmax - new_xmin) / cell_size)
grid_height = math.ceil((new_ymax - new_ymin) / cell_size)
return (new_xmin, new_ymin), grid_width, grid_height

Check warning on line 708 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L702-L708

Added lines #L702 - L708 were not covered by tests


def _geom_to_cells(
Expand All @@ -671,29 +718,29 @@
"""
Determine grid cell indices that intersect the given geometry.
"""
if isinstance(geom, shapely.geometry.Point):
sx = int((geom.x - origin[0]) // cell_size)
sy = int((geom.y - origin[1]) // cell_size)
return [(sx, sy)] if 0 <= sx < grid_width and 0 <= sy < grid_height else []

Check warning on line 724 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L721-L724

Added lines #L721 - L724 were not covered by tests

else:
minx, miny, maxx, maxy = geom.bounds
start_x = max(0, int((minx - origin[0]) // cell_size))
start_y = max(0, int((miny - origin[1]) // cell_size))
end_x = min(grid_width, int(math.ceil((maxx - origin[0]) / cell_size)))
end_y = min(grid_height, int(math.ceil((maxy - origin[1]) / cell_size)))

Check warning on line 731 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L727-L731

Added lines #L727 - L731 were not covered by tests

x_range = np.arange(start_x, end_x)
y_range = np.arange(start_y, end_y)
xx, yy = np.meshgrid(x_range, y_range)
candidate_polys = GeoSeries(

Check warning on line 736 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L733-L736

Added lines #L733 - L736 were not covered by tests
[
_get_cell_polygon(x, y, cell_size, origin)
for x, y in zip(xx.flatten(), yy.flatten())
]
)
mask = candidate_polys.intersects(geom)
return list(zip(xx.flatten()[mask], yy.flatten()[mask]))

Check warning on line 743 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L742-L743

Added lines #L742 - L743 were not covered by tests


def _enqueue_neighbors(
Expand All @@ -708,10 +755,10 @@
"""
Enqueue valid neighboring cells for BFS expansion.
"""
for nx, ny in _get_neighbors(x, y, grid_width, grid_height, mode=neighbor_mode):
if states[ny, nx] == CellState.UNKNOWN.value:
states[ny, nx] = CellState.FRONTIER.value
queue.append((nx, ny))

Check warning on line 761 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L758-L761

Added lines #L758 - L761 were not covered by tests


def _assign_adjacent_seed_cells(
Expand All @@ -720,26 +767,26 @@
"""
Reassign border and barrier cells to the proximate seed areas using a voting mechanism.
"""
new_states = states.copy()
indices = np.argwhere(

Check warning on line 771 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L770-L771

Added lines #L770 - L771 were not covered by tests
np.isin(states, [CellState.BARRIER.value, CellState.BOUNDARY.value])
)
grid_height, grid_width = states.shape

Check warning on line 774 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L774

Added line #L774 was not covered by tests

for y, x in indices:
neighbor_seeds = [

Check warning on line 777 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L776-L777

Added lines #L776 - L777 were not covered by tests
states[ny, nx]
for nx, ny in _get_neighbors(
x, y, grid_width, grid_height, mode=neighbor_mode
)
if states[ny, nx] >= 0
]
if neighbor_seeds:
cnt = Counter(neighbor_seeds)

Check warning on line 785 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L784-L785

Added lines #L784 - L785 were not covered by tests
# In case of ties, choose the smaller seed id.
chosen_seed = min(cnt.items(), key=lambda item: (-item[1], item[0]))[0]
new_states[y, x] = chosen_seed
return new_states

Check warning on line 789 in momepy/functional/_elements.py

View check run for this annotation

Codecov / codecov/patch

momepy/functional/_elements.py#L787-L789

Added lines #L787 - L789 were not covered by tests


def verify_tessellation(tessellation, geometry):
Expand Down
Loading