Looking at the Expected result
, I can say that, no spatial predicate available to operate and get that kind of result. Intersects
or touches
between 2 geometries will get some false-positive results.
Here I implement a simple check, same_lineQ(x1y1, x2y2)
, as a procedure to use in place where the missing spatial op is needed.
# PART 1
import geopandas as gpd
from shapely.geometry import Polygon, LineString, Point
import matplotlib.pyplot as plt
import pandas as pd
polys = gpd.GeoSeries([
Polygon([(0,0), (2,0), (2, 1.5), (2,2), (0,2)]),
Polygon([(0,2), (2,2), (2,4), (0,4)]),
Polygon([(2,0), (5,0), (5,1.5), (2,1.5)]),
Polygon([(3,3), (5,3), (5,5), (3,5)])
])
fp = gpd.GeoDataFrame({
'geometry': polys,
'name': ['a', 'b', 'c', 'd'],
'grnd': [25, 25, 25, 25],
'rf': [29, 35, 26, 31]
})
fig, ax = plt.subplots(figsize=(5/2, 5/2))
fp.plot(ax=ax, alpha=0.3, cmap='tab10', edgecolor='k',)
fp.apply(lambda x: ax.annotate(text=x['name'], xy=x.geometry.centroid.coords[0], ha='center'), axis=1)
# Part 2
# Collect all the line segments from all polygons
def get_all_xy0xy1(p1, attrib="none"):
"""
p1: a Polygon object, has single `exterior`
returns: list of [[x0,y0],[x1,y1]] ready for LineString creation
eg.: LineString([(0, 0), (9, 9)])
"""
xy0_xy1_list = []
attribs = []
for ix,xy in enumerate(zip(p1.exterior.xy[0], p1.exterior.xy[1])):
# 3 or more items
#print(ix,xy) #either x, or y separately
if ix>0:
#print([prev, xy]) #list of x,y; from-to
xy0_xy1_list.append([prev, xy])
attribs.append(attrib)
prev = xy
return xy0_xy1_list, attribs
# Line segments are collected in `all_line_segs`
all_line_segs = []
names = []
for ix, row in fp.iterrows():
name = row['name']
geom = row.geometry
all_line_segs += get_all_xy0xy1(geom, name)[0]
names += get_all_xy0xy1(geom, name)[1]
# Create a dataframe using the line segments
line_segs = pd.DataFrame({
'xy1_xy2': all_line_segs,
'name': names
})
# Part 3
def same_lineQ(x1y1, x2y2):
"""
Input: x1y1, x2y2; two list of (x,y).
Returns:
True if they represent the same LineString
ignoring the direction
else returns False.
"""
return (x1y1[0] in x2y2) and (x1y1[1] in x2y2)
for ir, irow in line_segs.iterrows():
iname = irow['name']
ixys = irow['xy1_xy2']
targets = set()
for kr, krow in line_segs.iterrows():
if ir != kr:
kname = krow['name']
if iname != kname:
kxys = krow['xy1_xy2']
if same_lineQ(ixys, kxys)==True:
#print(ixys, kxys, same_lineQ(ixys, kxys))
targets.update(kname)
else:
pass
if len(targets)==0:
print(ixys[0][0],ixys[0][1], ixys[1][0],ixys[1][1], iname)
else:
print(ixys[0][0],ixys[0][1], ixys[1][0],ixys[1][1], iname, targets.pop())
Output:
0.0 0.0 2.0 0.0 a
2.0 0.0 2.0 1.5 a c
2.0 1.5 2.0 2.0 a
2.0 2.0 0.0 2.0 a b
0.0 2.0 0.0 0.0 a
0.0 2.0 2.0 2.0 b a
2.0 2.0 2.0 4.0 b
2.0 4.0 0.0 4.0 b
0.0 4.0 0.0 2.0 b
2.0 0.0 5.0 0.0 c
5.0 0.0 5.0 1.5 c
5.0 1.5 2.0 1.5 c
2.0 1.5 2.0 0.0 c a
3.0 3.0 5.0 3.0 d
5.0 3.0 5.0 5.0 d
5.0 5.0 3.0 5.0 d
3.0 5.0 3.0 3.0 d
Edit
If the geodataframe is more complex, for example, some polygons have holes, or some rows have MultiPolygon instead of Polygon, The code above won't work. It only work with geodataframe that have polygons without holes only.
So what to do in such situation?
One approach is to explode the rows that do not have simple polygon, i.e. have polygon with holes or multipolygon, and get a resulting geodataframe that have all simple polygons.
Take this modified geodataframe as an example:

(Note e
is MultiPolygon of 2 Polygons, one of it has single hole. And f
is simple Polygon.)
If it is used with the code above, the output will be:
0.0 0.0 2.0 0.0 a
2.0 0.0 2.0 1.5 a c
2.0 1.5 2.0 2.0 a
2.0 2.0 0.0 2.0 a b
0.0 2.0 0.0 0.0 a
0.0 2.0 2.0 2.0 b a
2.0 2.0 2.0 4.0 b
2.0 4.0 0.0 4.0 b
0.0 4.0 0.0 2.0 b
2.0 0.0 5.0 0.0 c
5.0 0.0 5.0 1.5 c
5.0 1.5 2.0 1.5 c
2.0 1.5 2.0 0.0 c a
3.0 3.0 5.0 3.0 d
5.0 3.0 5.0 5.0 d e
5.0 5.0 3.0 5.0 d
3.0 5.0 3.0 3.0 d
6.5 0.0 7.5 0.0 f
7.5 0.0 7.5 1.5 f
7.5 1.5 6.5 1.5 f e
6.5 1.5 6.5 0.0 f
6.0 1.0 8.0 1.0 e
8.0 1.0 8.0 3.0 e
8.0 3.0 6.0 3.0 e
6.0 3.0 6.0 1.0 e
5.0 3.0 8.0 3.0 e
8.0 3.0 8.0 5.0 e
8.0 5.0 5.0 5.0 e
5.0 5.0 5.0 3.0 e d
6.5 2.5 7.5 2.5 e
7.5 2.5 7.5 1.5 e
7.5 1.5 6.5 1.5 e f
6.5 1.5 6.5 2.5 e