Joost Döbken
Joost Döbken

Reputation: 4017

Split self-intersecting linestring into non-self-intersecting linestrings

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]
]

My attempt

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

Answers (1)

Joost Döbken
Joost Döbken

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.]])
  • any combination of split indexes is given by: 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. enter image description here

This is a better legal (no zeros) split where the sum of the two boxes is 9+9=18

enter image description here

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

Related Questions