Shapely: Split LineString at arbitrary point along edge - python

I'm trying to split a Shapely LineString at the nearest point to some other coordinate. I can get the closest point on the line using project and interpolate but I am unable to split the line at this point as it is not a vertex.
I need to split the line along the edge, not snapped to the nearest vertex, so that the nearest point becomes a new vertex on the line.
Here's what I've done so far:
from shapely.ops import split
from shapely.geometry import Point, LineString
line = LineString([(0, 0), (5,8)])
point = Point(2,3)
# Find coordinate of closest point on line to point
d = line.project(point)
p = line.interpolate(d)
print(p)
# >>> POINT (1.910112359550562 3.056179775280899)
# Split the line at the point
result = split(line, p)
print(result)
# >>> GEOMETRYCOLLECTION (LINESTRING (0 0, 5 8))
Thanks!

As it turns out the answer I was looking for was outlined in the documentation as the cut method:
def cut(line, distance):
# Cuts a line in two at a distance from its starting point
if distance <= 0.0 or distance >= line.length:
return [LineString(line)]
coords = list(line.coords)
for i, p in enumerate(coords):
pd = line.project(Point(p))
if pd == distance:
return [
LineString(coords[:i+1]),
LineString(coords[i:])]
if pd > distance:
cp = line.interpolate(distance)
return [
LineString(coords[:i] + [(cp.x, cp.y)]),
LineString([(cp.x, cp.y)] + coords[i:])]
Now I can use the projected distance to cut the LineString:
...
d = line.project(point)
# print(d) 3.6039927920216237
cut(line, d)
# LINESTRING (0 0, 1.910112359550562 3.056179775280899)
# LINESTRING (1.910112359550562 3.056179775280899, 5 8)

Related

Numpy lines intersects circles

I have n lines and m circles.
I have a [n,2] numpy array of line start points:
[x1_1,y1_1],
[x1_2,y1_2],
...
[x1_n,y1_n]
And a [n,2] numpy array of line end points:
[x2_1,y2_1],
[x2_2,y2_2],
...
[x2_n,y2_n]
And an [m,2] numpy array of circle centers:
[cx_1,cy_1],
...
[cx_m,cy_m]
And an [m,1] numpy array of circle radii:
[cr_1...cr_m]
I would like to efficiently get an [n,m] numpy array where array[i,j] is True if line i intersects circle j.
In general I would take the normalised perpendicular vector to each line and take the dot product of that with each (xi,yi) - (cx_j,cy_y) and ask if it's less than cr_i; but I also have to check whether that implied point is on the line and check each end individually if not. I'm wondering if there is a more elegant solution.
Ok, assume to have these shape
start = (np.random.random((3,2))-.5)*5
end = (np.random.random((3,2))-.5)*5
center = (np.random.random((4,2))-.5)*5
radius = np.random.random((4,1))*3
for each center we can compute the distance from the three lines by:
D = np.array([
np.linalg.norm(np.cross(end-start, start-c).reshape(-1,1),axis=1)/np.linalg.norm((end-start).reshape(-1,2), axis=1)
for c in center
]).T
D[i,j] will be the distance between line i (in rows) and center j (in clumns).
Now we can simply compare this distances to the radius distances with:
I = (d<radius.repeat(len(start), axis=1).T)
I is a matrix of the same shape of D; I[i,j] is True if the distance between the line i and the center j is lower than the radius j (and so if the line i intersect the circle j) and False otherwise.
I know it is not very elegant, but I hope it can be useful.
I can't think of a substantially simpler algorithm than the one outlined in the question. As far as the implementation is concerned, it is easy to make these computations using shapely.
First, lets generate and plot some sample data:
from itertools import product
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import matplotlib.colors as mcolors
from shapely.geometry import LineString, Point, MultiLineString
import numpy as np
import pandas as pd
# generate data
rng = np.random.default_rng(123)
start = (rng.random((3, 2)) - .5) * 5
end = (rng.random((3, 2)) - .5) * 5
center = (rng.random((4, 2)) - .5) * 5
radius = rng.random((4, 1)) * 3
# plot lines and circles
fig, ax = plt.subplots()
fig.set_size_inches(8, 8)
ax.set_aspect('equal')
colors = list(mcolors.TABLEAU_COLORS.keys())
for i, ends in enumerate(zip(start, end)):
ax.plot(*zip(*ends), label=f"line {i}")
for i, (c, r) in enumerate(zip(center, radius)):
ax.add_patch(Circle(c, r, fill=False, ec=colors[i], label=f"circle {i}"))
plt.legend()
plt.show()
This gives:
Next, compute the array of intersections, with rows corresponding to lines and columns corresponding to circles:
lines = [LineString(ends) for ends in list(zip(start, end))]
circles = [Point(c).buffer(r).boundary for c, r in zip(center, radius)]
out = np.empty((len(lines), len(circles)), dtype=bool)
for i, (l, c) in enumerate(product(lines, circles)):
out[np.unravel_index(i, out.shape)] = l.intersects(c)
#convert to a dataframe for better display
df = pd.DataFrame(out)
df.index.name = 'lines'
df.columns.name = 'circles'
print(df)
The result:
circles 0 1 2 3
lines
0 True False True True
1 False False False True
2 False False False False

Create a square Polygon from lower left corner coordinates

I do have coordinates (lat, lon) and I need to create polygon of 100 meters.
For now I can only create a polygon (square) where the point is the center of it and not the lower left corner.
import geopandas as gpd
from shapely.geometry import Point
# Generate some sample data
p1 = Point((-122.431297, 37.773972))
points = gpd.GeoSeries([p1])
# Buffer the points using a square cap style
# Note cap_style: round = 1, flat = 2, square = 3
buffer = points.buffer(1, cap_style = 3)
Use geopy to generate/construct the remaining points.
import geopy
import geopy.distance as distance
from shapely.geometry import Polygon
#Assume each site is 100m (question not clear)
d = distance.distance(kilometers=100/1000)
# Going clockwise, from lower-left to upper-left, upper-right...
p1 = geopy.Point(( 37.773972, -122.431297))
p2 = d.destination(point=p1, bearing=0)
p3 = d.destination(point=p2, bearing=90)
p4 = d.destination(point=p3, bearing=180)
points = [(p.latitude, p.longitude) for p in [p1,p2,p3,p4]]
polygon = Polygon(points)

How to get equally spaced points on a line in Shapely

I'm trying to (roughly) equally space the points of a line to a predefined distance.
It's ok to have some tolerance between the distances but as close as possible would be desirable.
I know I could manually iterate through each point in my line and check the p1 distance vs p2 and add more points if needed.
But I wondered if anyone knows if there is a way to achieve this with shapely as I already have the coords in a LineString.
One way to do that is to use interpolate method that returns points at specified distances along the line. You just have to generate a list of the distances somehow first. Taking the input line example from Roy2012's answer:
import numpy as np
from shapely.geometry import LineString
from shapely.ops import unary_union
line = LineString(([0, 0], [2, 1], [3, 2], [3.5, 1], [5, 2]))
Splitting at a specified distance:
distance_delta = 0.9
distances = np.arange(0, line.length, distance_delta)
# or alternatively without NumPy:
# points_count = int(line.length // distance_delta) + 1
# distances = (distance_delta * i for i in range(points_count))
points = [line.interpolate(distance) for distance in distances] + [line.boundary[1]]
multipoint = unary_union(points) # or new_line = LineString(points)
Note that since the distance is fixed you can have problems at the end of the line as shown in the image. Depending on what you want you can include/exclude the [line.boundary[1]] part which adds the line's endpoint or use distances = np.arange(0, line.length, distance_delta)[:-1] to exclude the penultimate point.
Also, note that the unary_union I'm using should be more efficient than calling object.union(other) inside a loop, as shown in another answer.
Splitting to a fixed number of points:
n = 7
# or to get the distances closest to the desired one:
# n = round(line.length / desired_distance_delta)
distances = np.linspace(0, line.length, n)
# or alternatively without NumPy:
# distances = (line.length * i / (n - 1) for i in range(n))
points = [line.interpolate(distance) for distance in distances]
multipoint = unary_union(points) # or new_line = LineString(points)
You can use the shapely substring operation:
from shapely.geometry import LineString
from shapely.ops import substring
line = LineString(([0, 0], [2, 1], [3,2], [3.5, 1], [5, 2]))
mp = shapely.geometry.MultiPoint()
for i in np.arange(0, line.length, 0.2):
s = substring(line, i, i+0.2)
mp = mp.union(s.boundary)
The result for this data is given below. Each circle is a point.

sort points in order to have a continuous curve using python

I have a list of unsorted points:
List = [(-50.6261, 74.3683), (-63.2489, 75.0038), (-76.0384, 75.6219), (-79.8451, 75.7855), (-30.9626, 168.085), (-27.381, 170.967), (-22.9191, 172.928), (-16.5869, 173.087), (-4.813, 172.505), (-109.056, 92.0063), (-96.0705, 91.4232), (-83.255, 90.8563), (-80.7807, 90.7498), (-54.1694, 89.5087), (-41.6419, 88.9191), (-32.527, 88.7737), (-27.6403, 91.0134), (-22.3035, 95.141), (-18.0168, 100.473), (-15.3918, 105.542), (-13.6401, 112.373), (-13.3475, 118.988), (-14.4509, 125.238), (-17.1246, 131.895), (-21.6766, 139.821), (-28.5735, 149.98), (-33.395, 156.344), (-114.702, 83.9644), (-114.964, 87.4599), (-114.328, 89.8325), (-112.314, 91.6144), (-109.546, 92.0209), (-67.9644, 90.179), (-55.2013, 89.5624), (-34.4271, 158.876), (-34.6987, 161.896), (-33.6055, 164.993), (-87.0365, 75.9683), (-99.8007, 76.0889), (-105.291, 76.5448), (-109.558, 77.3525), (-112.516, 79.2509), (-113.972, 81.3335), (2.30014, 171.635), (4.40918, 169.691), (5.07165, 166.974), (5.34843, 163.817), (5.30879, 161.798), (-29.6746, 73.5082), (-42.5876, 74.0206)]
I want to sort those points to have a continuous curve passing by every point just once, starting from start = (-29.6746, 73.5082)
and end = (5.30879, 161.798)
This is what I tried so far:
import matplotlib.pyplot as plt
import numpy as np
from sklearn.neighbors import NearestNeighbors
import networkx as nx
for el in List:
X.append(el[0])
Y.append(el[1])
x = np.array(X)
y = np.array(Y)
points = np.c_[x, y]
# find 2 nearest neighbors
clf = NearestNeighbors(2).fit(points)
G = clf.kneighbors_graph()
T = nx.from_scipy_sparse_matrix(G)
# indexes of the new order
order = list(nx.dfs_preorder_nodes(T, 0))
# sorted arrays
new_x = x[order]
new_y = y[order]
plt.plot(new_x, new_y)
plt.show()
But I still get an unsorted list, and I couldn't find a way to determine the start point and end point.
We can see the problem as a Traveling salesman problem, that we can optimize by looking for the nearest point
def distance(P1, P2):
"""
This function computes the distance between 2 points defined by
P1 = (x1,y1) and P2 = (x2,y2)
"""
return ((P1[0] - P2[0])**2 + (P1[1] - P2[1])**2) ** 0.5
def optimized_path(coords, start=None):
"""
This function finds the nearest point to a point
coords should be a list in this format coords = [ [x1, y1], [x2, y2] , ...]
"""
if start is None:
start = coords[0]
pass_by = coords
path = [start]
pass_by.remove(start)
while pass_by:
nearest = min(pass_by, key=lambda x: distance(path[-1], x))
path.append(nearest)
pass_by.remove(nearest)
return path
# define a start point
start = [x0, y0]
path = optimized_path(List,start)
Not an answer, but too much for a comment
I plotted the data points as scatter and line
I see a visually smooth (low order local derivatve spline curve) with ~10% points 'out of order'
Is this typical of the problem?, is the data mostly in order?
How general or specific does the code have to be
I don't know the "big hammer" libs, but cleaned up the surounding code and did the same plot
List = [(-50.6261, 74.3683), (-63.2489, 75.0038), (-76.0384, 75.6219), (-79.8451, 75.7855), (-30.9626, 168.085), (-27.381, 170.967), (-22.9191, 172.928), (-16.5869, 173.087), (-4.813, 172.505), (-109.056, 92.0063), (-96.0705, 91.4232), (-83.255, 90.8563), (-80.7807, 90.7498), (-54.1694, 89.5087), (-41.6419, 88.9191), (-32.527, 88.7737), (-27.6403, 91.0134), (-22.3035, 95.141), (-18.0168, 100.473), (-15.3918, 105.542), (-13.6401, 112.373), (-13.3475, 118.988), (-14.4509, 125.238), (-17.1246, 131.895), (-21.6766, 139.821), (-28.5735, 149.98), (-33.395, 156.344), (-114.702, 83.9644), (-114.964, 87.4599), (-114.328, 89.8325), (-112.314, 91.6144), (-109.546, 92.0209), (-67.9644, 90.179), (-55.2013, 89.5624), (-34.4271, 158.876), (-34.6987, 161.896), (-33.6055, 164.993), (-87.0365, 75.9683), (-99.8007, 76.0889), (-105.291, 76.5448), (-109.558, 77.3525), (-112.516, 79.2509), (-113.972, 81.3335), (2.30014, 171.635), (4.40918, 169.691), (5.07165, 166.974), (5.34843, 163.817), (5.30879, 161.798), (-29.6746, 73.5082), (-42.5876, 74.0206)]
import matplotlib.pyplot as plt
import numpy as np
from sklearn.neighbors import NearestNeighbors
import networkx as nx
points = np.asarray(List)
# find 2 nearest neighbors
clf = NearestNeighbors(2).fit(points)
G = clf.kneighbors_graph()
T = nx.from_scipy_sparse_matrix(G)
# indexes of the new order
order = list(nx.dfs_preorder_nodes(T, 0))
# sorted arrays
new_points = points[order]
plt.scatter(*zip(*points))
plt.plot(*zip(*new_points), 'r')
plt.show()

Shapely: Cut a piece from a linestring at two cutting points

the well known function
from shapely.geometry import *
from shapely.wkt import loads
def cut(line, distance):
# Cuts a line in two at a distance from its starting point
if distance <= 0.0 or distance >= line.length:
return [LineString(line)]
coords = list(line.coords)
for i, p in enumerate(coords):
pd = line.project(Point(p))
if pd == distance:
return [
LineString(coords[:i+1]),
LineString(coords[i:])]
if pd > distance:
cp = line.interpolate(distance)
return [
LineString(coords[:i] + [(cp.x, cp.y)]),
LineString([(cp.x, cp.y)] + coords[i:])]
splits a shapely linestring into two lines at distance.
What I need to do is to cut a piece of a certain length from the line, at a certain position along the line
Example line:
line = loads("LINESTRING (12.0133696 47.8217147, 12.0132944 47.8216655, 12.0132056 47.8215749, 12.0131542 47.8215034, 12.0130522 47.8212931, 12.0129941 47.8211294, 12.0130381 47.8209553, 12.0131116 47.8208718, 12.013184 47.8208107, 12.0133547 47.8207312, 12.0135537 47.8206727, 12.013915 47.8206019, 12.0141624 47.8205671, 12.0144317 47.8204965)")
I tried an approach with getting the difference between some linestrings that i got by appliyng above cut functio, but the results are not good due to shapely limitations.
Any ideas?
I'll answer myself, and am happy about improvements:
def cut_piece(line,distance, lgth):
""" From a linestring, this cuts a piece of length lgth at distance.
Needs cut(line,distance) func from above ;-)
"""
precut = cut(line,distance)[1]
result = cut(precut,lgth)[0]
return result
You can also use intersection and normalize the interpolate to get a curve within specific range :
def cut(line, distance):
if distance <= 0.0 :#line.length:
return [None, LineString(line)]
elif distance >= 1.0:
return [LineString(line), None]
coords = list(line.coords)
for i, p in enumerate(coords):
pd = line.project(Point(p), normalized=True)
if pd == distance:
return [
LineString(coords[:i+1]),
LineString(coords[i:])]
if pd > distance:
cp = line.interpolate(distance, normalized=True)
return [
LineString(coords[:i] + [(cp.x, cp.y)]),
LineString([(cp.x, cp.y)] + coords[i:])]
def cut_piece(line,distance1, distance2):
""" From a linestring, this cuts a piece of length lgth at distance.
Needs cut(line,distance) func from above ;-)
"""
l1 = cut(line, distance1)[1]
l2 = cut(line, distance2)[0]
result = l1.intersection(l2)
return result
Example:
cir = Point(0.0, 0.0).buffer(200).exterior
cir.coords = list(cir.coords)[::-1] # to flip the curve direction
print(cir.is_ccw)
d = cut_piece(cir,0.25, 0.75)
example

Categories

Resources