Skip to content

Commit

Permalink
ENH: support single-part multipolygons in Squareness and CentroidCorn…
Browse files Browse the repository at this point in the history
…ers (#507)

* adds multipolygon support for Squareness and CentroidCorners

* Update momepy/shape.py

Adds more explanation regarding application to MultiPolygons

Co-authored-by: James Gaboardi <jgaboardi@gmail.com>

* Update momepy/shape.py

Adds more explanatory text on application to MultiPolygons

Co-authored-by: James Gaboardi <jgaboardi@gmail.com>

* fixes doc line spacing; moves len(points) to var.

---------

Co-authored-by: James Gaboardi <jgaboardi@gmail.com>
  • Loading branch information
songololo and jGaboardi authored Aug 21, 2023
1 parent cd9189b commit 4f1176a
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 82 deletions.
149 changes: 70 additions & 79 deletions momepy/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,8 @@ class Squareness:
"""
Calculates the squareness of each object in a given GeoDataFrame. Uses only
external shape (``shapely.geometry.exterior``), courtyards are not included.
Returns ``np.nan`` for MultiPolygons.
Returns ``np.nan`` for true MultiPolygons (containing multiple geometries).
MultiPolygons with a singular geometry are treated as Polygons.
.. math::
\\mu=\\frac{\\sum_{i=1}^{N} d_{i}}{N}
Expand Down Expand Up @@ -948,41 +949,37 @@ def _angle(a, b, c):

return angle

def _calc(geom):
angles = []
points = list(geom.exterior.coords) # get points of a shape
n_points = len(points)
if n_points < 3:
return np.nan
stop = n_points - 1
for i in range(
1, n_points
): # for every point, calculate angle and add 1 if True angle
a = np.asarray(points[i - 1])
b = np.asarray(points[i])
# in last case, needs to wrap around start to find finishing angle
c = np.asarray(points[i + 1]) if i != stop else np.asarray(points[1])
ang = _angle(a, b, c)
if ang <= 175 or ang >= 185:
angles.append(ang)
else:
continue
deviations = [abs(90 - i) for i in angles]
return np.mean(deviations)

# fill new column with the value of area, iterating over rows one by one
for geom in tqdm(gdf.geometry, total=gdf.shape[0], disable=not verbose):
if geom.geom_type == "Polygon":
angles = []
points = list(geom.exterior.coords) # get points of a shape
stop = len(points) - 1 # define where to stop
for i in np.arange(
len(points)
): # for every point, calculate angle and add 1 if True angle
if i == 0:
continue
elif i == stop:
a = np.asarray(points[i - 1])
b = np.asarray(points[i])
c = np.asarray(points[1])
ang = _angle(a, b, c)

if ang <= 175 or _angle(a, b, c) >= 185:
angles.append(ang)
else:
continue

else:
a = np.asarray(points[i - 1])
b = np.asarray(points[i])
c = np.asarray(points[i + 1])
ang = _angle(a, b, c)

if _angle(a, b, c) <= 175 or _angle(a, b, c) >= 185:
angles.append(ang)
else:
continue
deviations = [abs(90 - i) for i in angles]
results_list.append(np.mean(deviations))

if geom.geom_type == "Polygon" or (
geom.geom_type == "MultiPolygon" and len(geom.geoms) == 1
):
# unpack multis with single geoms
if geom.geom_type == "MultiPolygon":
geom = geom.geoms[0]
results_list.append(_calc(geom))
else:
results_list.append(np.nan)

Expand Down Expand Up @@ -1117,7 +1114,8 @@ def __init__(self, gdf):
class CentroidCorners:
"""
Calculates the mean distance centroid - corners and standard deviation.
Returns ``np.nan`` for MultiPolygons.
Returns ``np.nan`` for true MultiPolygons (containing multiple geometries).
MultiPolygons with a singular geometry are treated as Polygons.
.. math::
\\overline{x}=\\frac{1}{n}\\left(\\sum_{i=1}^{n} dist_{i}\\right);
Expand Down Expand Up @@ -1173,55 +1171,48 @@ def true_angle(a, b, c):
return True
return False

def _calc(geom):
distances = [] # set empty list of distances
centroid = geom.centroid # define centroid
points = list(geom.exterior.coords) # get points of a shape
n_points = len(points)
stop = n_points - 1 # define where to stop
for i in range(
1, n_points
): # for every point, calculate angle and add 1 if True angle
a = np.asarray(points[i - 1])
b = np.asarray(points[i])
# in last case, needs to wrap around start to find finishing angle
c = np.asarray(points[i + 1]) if i != stop else np.asarray(points[1])
p = Point(points[i])
# calculate distance point - centroid
if true_angle(a, b, c) is True:
distances.append(centroid.distance(p))
else:
continue
return distances

# iterating over rows one by one
for geom in tqdm(gdf.geometry, total=gdf.shape[0], disable=not verbose):
if geom.geom_type == "Polygon":
distances = [] # set empty list of distances
centroid = geom.centroid # define centroid
points = list(geom.exterior.coords) # get points of a shape
stop = len(points) - 1 # define where to stop
for i in np.arange(
len(points)
): # for every point, calculate angle and add 1 if True angle
if i == 0:
continue
elif i == stop:
a = np.asarray(points[i - 1])
b = np.asarray(points[i])
c = np.asarray(points[1])
p = Point(points[i])

if true_angle(a, b, c) is True:
distance = centroid.distance(
p
) # calculate distance point - centroid
distances.append(distance) # add distance to the list
else:
continue

else:
a = np.asarray(points[i - 1])
b = np.asarray(points[i])
c = np.asarray(points[i + 1])
p = Point(points[i])

if true_angle(a, b, c) is True:
distance = centroid.distance(p)
distances.append(distance)
else:
continue
if not distances: # circular buildings
if geom.has_z:
coords = [
(coo[0], coo[1]) for coo in geom.convex_hull.exterior.coords
]
else:
coords = geom.convex_hull.exterior.coords
if geom.geom_type == "Polygon" or (
geom.geom_type == "MultiPolygon" and len(geom.geoms) == 1
):
# unpack multis with single geoms
if geom.geom_type == "MultiPolygon":
geom = geom.geoms[0]
distances = _calc(geom)
# circular buildings
if not distances:
# handle z dims
coords = [
(coo[0], coo[1]) for coo in geom.convex_hull.exterior.coords
]
results_list.append(_circle_radius(coords))
results_list_sd.append(0)
# calculate mean and std dev
else:
results_list.append(np.mean(distances)) # calculate mean
results_list_sd.append(np.std(distances)) # calculate st.dev
results_list.append(np.mean(distances))
results_list_sd.append(np.std(distances))
else:
results_list.append(np.nan)
results_list_sd.append(np.nan)
Expand Down
21 changes: 18 additions & 3 deletions momepy/tests/test_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import geopandas as gpd
import numpy as np
import pytest
from shapely.geometry import MultiLineString, Point, Polygon
from shapely.geometry import MultiLineString, Point, Polygon, MultiPolygon

import momepy as mm
from momepy.shape import _circle_area
Expand Down Expand Up @@ -169,6 +169,12 @@ def test_Squareness(self):
assert self.df_buildings["squ"][0] == check
self.df_buildings["squ"] = mm.Squareness(self.df_buildings.exterior).series
assert self.df_buildings["squ"].isna().all()
df_buildings_multi = self.df_buildings.copy()
df_buildings_multi["geometry"] = df_buildings_multi["geometry"].apply(
lambda geom: MultiPolygon([geom])
)
self.df_buildings["squm"] = mm.Squareness(df_buildings_multi).series
assert self.df_buildings["squm"][0] == check

def test_EquivalentRectangularIndex(self):
self.df_buildings["eri"] = mm.EquivalentRectangularIndex(
Expand Down Expand Up @@ -196,13 +202,22 @@ def test_CentroidCorners(self):
0,
0,
]
check = pytest.approx(15.961, rel=1e-3)
check_devs = pytest.approx(3.081, rel=1e-3)
cc = mm.CentroidCorners(self.df_buildings)
self.df_buildings["ccd"] = cc.mean
self.df_buildings["ccddev"] = cc.std
check = pytest.approx(15.961, rel=1e-3)
check_devs = pytest.approx(3.081, rel=1e-3)
assert self.df_buildings["ccd"][0] == check
assert self.df_buildings["ccddev"][0] == check_devs
df_buildings_multi = self.df_buildings.copy()
df_buildings_multi["geometry"] = df_buildings_multi["geometry"].apply(
lambda geom: MultiPolygon([geom])
)
cc = mm.CentroidCorners(df_buildings_multi)
df_buildings_multi["ccd"] = cc.mean
df_buildings_multi["ccddev"] = cc.std
assert df_buildings_multi["ccd"][0] == check
assert df_buildings_multi["ccddev"][0] == check_devs

def test_Linearity(self):
self.df_streets["lin"] = mm.Linearity(self.df_streets).series
Expand Down

0 comments on commit 4f1176a

Please sign in to comment.