Python – How to Associate a Point to a Linestring Using the Original Linestring Order in GeoPandas

geopandaspythonshapely

Using Geopandas, Shapely

Suppose I have a Linestring that represents a (cornered) street:

LineString([(1, 1), (2, 2), (3, 1)]

Note that the order of points in this linestring matters because LineString([(1, 1), (3, 1), (2, 2)] would represent a very different street.

Now, suppose I have list of points that belong to my street:

Point((1.9, 1.9))

Point((1.5, 1.5))

Point((2.5, 1.5))

Point((1.2, 1.2))

I want to create a new Linestring where all the Points are "merged" with the original street coordinates. This "merge" mechanism has to maintain the original street shape as follows:

LineString([(1, 1), (1.2, 1.2), (1.5, 1.5), (1.9, 1.9), (2, 2), (2.5, 1.5). (3, 1)]

Any ideas how to approach this?

import geopandas as gpd
from shapely.geometry import Point, LineString
street = gpd.GeoDataFrame({'street': ['st'], 'geometry': LineString([(1, 1), (2, 2), (3, 1)])})
pp = gpd.GeoDataFrame({'geometry': [Point((1.9, 1.9)), Point((1.5, 1.5)), Point((2.5, 1.5)), Point((1.2, 1.2))]})
print(street)
print(pp)

Best Answer

Here's a lengthy-ish approach if you already know which points belong to which streets:

import pandas as pd
import geopandas as gpd
import shapely

street = shapely.geometry.LineString([(1, 1), (2, 2), (3, 1)])
pp = gpd.GeoDataFrame({'geometry': [shapely.geometry.Point(pt) for pt in [(1.9, 1.9), 
                                                                          (1.5, 1.5), 
                                                                          (2.5, 1.5), 
                                                                          (1.2, 1.2)]],
                       'origin':'external'},
                      geometry='geometry')

ps = gpd.GeoDataFrame({'origin':'street',
                       'geometry':[shapely.geometry.Point(pt) for pt in list(street.coords)]},
                      geometry='geometry')

p_all = pd.concat([pp, ps], ignore_index=True)

p_all['dist_along_line'] = p_all['geometry'].apply(lambda p: street.project(p))

p_all = p_all.sort_values(by='dist_along_line')

final_geom = shapely.geometry.LineString(p_all['geometry'].values)

print(p_all)

print(final_geom.wkt)

Note how, in my code, the street variable isn't a GeoDataFrame but a simple shapely Linestring instead.

The final_geom variable will contain the sorted points as you wanted, while the p_all variable will contain a GeoDataFrame with the sorted list of all the points. I've also added a "origin" column so you can tell which points came from the original street geometry and which ones came from the pp GeoDataFrame of external points.

plotted line and resulting geodataframe

Caveat

Note that this approach will only work with LineStrings. If some streets are coded as MultiLineStrings, you'll have to take care of them differently in the ps = .... statement.

Related Question