Reputation: 4017
I have a list of coordinates defining a line string that might intersect with itself:
coordinates = [
[0, 3],
[0, 5],
[4, 5],
[4, 0],
[0, 0],
[0, 5],
[2, 5]
]
How can I split the linestring into smaller linestrings so none of the linestrings intersects with itself?
the desired outcome in this case would be:
line0 = [
[0, 3],
[0, 5],
[4, 5],
[4, 0]
]
line1 = [
[4, 0],
[0, 0],
[0, 5],
[2, 5]
]
In my attempt so far I construct an intersection matrix using Shapely Linestrings to find the intersections:
from shapely.geometry import LineString
from itertools import product, zip_longest
import numpy as np
def get_intersection_matrix(coordinates):
linestrings = [
(ix, LineString([c0, c1]))
for ix, (c0, c1) in enumerate(zip(coordinates[:-1], coordinates[1:]))
]
M = np.zeros((len(linestrings), len(linestrings)))
for (ix0, ls0), (ix1, ls1) in combinations(linestrings, 2):
if abs(ix0 - ix1) == 1: # ignore connecting segments
continue
if ls0.intersects(ls1):
M[ix0, ix1], M[ix1, ix0] = 1, 1
return M
which outputs what I call the "intersection matrix":
>> get_intersection_matrix(coordinates)
array([[0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0]])
That you can read as:
Also; I think that the number of "intersection clusters" indicate the number of linestrings: no_clusters + 1
Upvotes: 1
Views: 271
Reputation: 4017
How I solve it now... I changed my intersection matrix, so at no intersection the value is 1 and at any intersection the value is 0.
def get_intersection_matrix(coordinates):
linestrings = [
(ix, LineString([c0, c1]))
for ix, (c0, c1) in enumerate(zip(coordinates[:-1], coordinates[1:]))
]
M = np.ones((len(linestrings), len(linestrings)))
for (ix0, ls0), (ix1, ls1) in combinations(linestrings, 2):
if abs(ix0 - ix1) == 1: # ignore connecting segments
continue
if ls0.intersects(ls1):
M[ix0, ix1], M[ix1, ix0] = 0, 0
return M
>> M = get_intersection_matrix(coordinates)
>> M
array([[1., 1., 1., 1., 0., 0.],
[1., 1., 1., 1., 0., 0.],
[1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1.],
[0., 0., 1., 1., 1., 1.],
[0., 0., 1., 1., 1., 1.]])
itertools.combinations(range(1, len(M)), nr_split_ixs)
where also ix1 < ix2 < ... < ixn
at one split index you get two squares that should not contain any 0's, and the squares can be optimized by a minimum sum!
This is a legal (but not the best) split with split_ix = 4
and the sum of the two boxes is 16+4 = 20.
This is a better legal (no zeros) split where the sum of the two boxes is 9+9=18
The method to calculate the scored split indexes:
def get_scored_split_ixs_combination(M, nr_split_ixs):
ixs_scores = []
for ixs in combinations(range(1, len(M)), nr_split_ixs):
splitted_matrix = [
M[i0:i1, i0:i1] for i0, i1 in zip((0, *ixs), (*ixs, len(M)))
]
# check if no matrices have zeros
if not all([(m > 0).all() for m in splitted_matrix]):
# ilegal ixs combination
continue
ixs_scores.append((ixs, sum([m.sum() for m in splitted_matrix])))
return ixs_scores
if the return is empty there are no legal options and you should increase the number of splits.
Now return the best split option by increment the number of splits:
def get_best_split_ixs_combination(M):
nr_split_ixs = 0
while True:
ixs_scores = get_scored_split_ixs_combination(M, nr_split_ixs)
if ixs_scores:
return min(ixs_scores, key=lambda x: x[1])[0]
nr_split_ixs +=1
>> get_best_split_ixs_combination(M)
(3,)
And finally wrap it all together:
def get_non_intersecting_linestrings(coordinates):
M = get_intersection_matrix(coordinates)
split_indexes = get_best_split_ixs_combination(M)
return [
coordinates[i1:i2]
for i1, i2 in zip([0] + split_indexes, split_indexes + [len(coordinates)])
]
>> get_non_intersecting_linestrings(coordinates)
[[[0, 3], [0, 5], [4, 5]], [[4, 0], [0, 0], [0, 5], [2, 5]]]
Upvotes: 0