Failing to find a solution, I ended up writing this algorithm. It does the job, but could handle branches better, ie. choose the branch that would produce the longest continuous path. Now it just sticks to first line segment and proceeds from there.
Given a GeoJSON MultiLineString geometry, the algorithm orders line segments into a continuous path and returns a new geometry.
The code is licensed under the Do What The F*ck You Want To Public License.
import math
from collections import namedtuple
from operator import attrgetter
from copy import deepcopy
def arrange_geometry(original_geometry):
def distance(coords1, coords2):
return math.sqrt(math.pow(coords1[0] - coords2[0], 2) + math.pow(coords1[1] - coords2[1], 2))
MinDistance = namedtuple('MinDistance', 'target distance offset reverse_target')
geometry = deepcopy(original_geometry)
if geometry['type'] == 'MultiLineString':
lines = geometry['coordinates']
sorted_multistring = [lines.pop(0)]
while lines:
min_distances = []
for line in lines:
source_a = sorted_multistring[0][0]
source_b = sorted_multistring[-1][-1]
target_a = line[0]
target_b = line[-1]
distances = [
MinDistance(target=line, distance=distance(source_b, target_a), offset=1, reverse_target=False),
MinDistance(target=line, distance=distance(source_a, target_a), offset=-1, reverse_target=True),
MinDistance(target=line, distance=distance(source_b, target_b), offset=1, reverse_target=True),
MinDistance(target=line, distance=distance(source_a, target_b), offset=-1, reverse_target=False)
]
min_distance = min(distances, key=attrgetter('distance'))
min_distances.append(min_distance)
min_distance = min(min_distances, key=attrgetter('distance'))
target = min_distance.target
if min_distance.reverse_target:
target.reverse()
if min_distance.offset == 1:
sorted_multistring.append(target)
else:
sorted_multistring.insert(0, target)
lines.remove(target)
geometry['coordinates'] = sorted_multistring
return geometry