3

I have a MultiLineString consisting of individual LineStrings that form a path. The path has a direction and LineStrings must be sorted to reflect this order. To do that some strings must be reversed to point to the same direction as the rest. What would be a suitable algorithm to do this task?

In other words, what would be the best way to sort a list of lists, where lists can be reversed? Ie

Input:

[2, 1] [4, 5] [0, 1] [5, 6] [9, 8]

Output:

[0, 1] [1, 2] [4, 5] [5, 6] [8, 9] 
kissaprofeetta
  • 348
  • 1
  • 2
  • 9
  • 1
    Is this JavaScript or Python related? Because I don't think both tags are necessary. – VLAZ Apr 30 '18 at 21:37

3 Answers3

3

Sorted() with list comprehension

Ex:

l = [[2, 1] ,[4, 5], [0, 1], [5, 6], [9, 8]]
print(sorted([sorted(i) for i in l]))

Output:

[[0, 1], [1, 2], [4, 5], [5, 6], [8, 9]]
Rakesh
  • 81,458
  • 17
  • 76
  • 113
  • My example was not clear enough I guess. As the input are a list of two dimensional coordinates, `sorted` cannot be used. The only operation that that could be applied to the inner lists is reverse and the algorithm should decide whether to reverse a list or leave it as is. – kissaprofeetta May 01 '18 at 07:48
  • Sorry i do not understand. If you need to reverse then you can try something like `print(sorted([list(reversed(i)) for i in l]))` – Rakesh May 01 '18 at 13:00
0

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
kissaprofeetta
  • 348
  • 1
  • 2
  • 9
0

Even though the question is requiring a Python solution, and since I reached this page through a DuckDuckGo search trying to find a way to solve this matter (sorting of segments on a GeoJson MultiLineString geometry), I think that a Java / Kotlin solution could be appreciated by other people getting here.

I initially came up with a solution of my own, which happens to be kind of similar to the one of @kissaprofeetta . Although I approach it with a few differences:

  • The distance algorithm I use, is a bit more exact and is intended to be used for Geolocation purposes, since it takes in account that the earth is not a 2D plane/map, but a sphere. In fact, elevation data could be easily added as well, to be even more precise.
  • The way of adding segments to the new MultiLineString array is a bit less fancier than @kissaprofeetta answer
  • I have added the reading of GeoJson files and the writting of the sorted GeoJson

    import com.google.gson.Gson
    import com.google.gson.GsonBuilder
    import org.xml.sax.SAXException
    import java.io.File
    import java.io.IOException
    import java.io.PrintWriter
    import javax.xml.parsers.ParserConfigurationException
    
    const val ADDITION_MODE_BEGINNING_TO_BEGINNING = 0
    const val ADDITION_MODE_BEGINNING_TO_END = 1
    const val ADDITION_MODE_END_TO_BEGINNING = 2
    const val ADDITION_MODE_END_TO_END = 3
    
    data class MultiLineString2(
        val type: String,
        val coordinates: ArrayList<ArrayList<ArrayList<Double>>>
    )
    
    fun sortSegmentsOfGeoJsonRoute(routeId: Int)
    {
        try {
            val gson = GsonBuilder().setPrettyPrinting().create()
            val geoJsonRoute = gson.fromJson(
                File("src/main/assets/geojson/" + routeId + ".geojson").readText(),
                MultiLineString2::class.java
            )
            val newTrackSegments = ArrayList<ArrayList<ArrayList<Double>>>()
            newTrackSegments.add(geoJsonRoute.coordinates.first())
    
            geoJsonRoute.coordinates.forEach { newSegment ->
                if (!newTrackSegments.contains(newSegment)) {
                    var existingSegmentAsReference: ArrayList<ArrayList<Double>>? = null
                    var minDistanceToNewSegment = Double.MAX_VALUE
                    var additionMode = 0
    
                    newTrackSegments.forEach { existingSegment ->
                        val existingSegmentEnd = existingSegment.lastIndex
                        val newSegmentEnd = newSegment.lastIndex
                        val distFromBeginningToBeginning = distance(existingSegment[0][1], newSegment[0][1], existingSegment[0][0], newSegment[0][0])
                        val distFromBeginningToEnd = distance(existingSegment[0][1], newSegment[newSegmentEnd][1], existingSegment[0][0], newSegment[newSegmentEnd][0])
                        val distFromEndToBeginning = distance(existingSegment[existingSegmentEnd][1], newSegment[0][1], existingSegment[existingSegmentEnd][0], newSegment[0][0])
                        val distFromEndToEnd = distance(existingSegment[existingSegmentEnd][1], newSegment[newSegmentEnd][1], existingSegment[existingSegmentEnd][0], newSegment[newSegmentEnd][0])
    
                        var curMinDistance = Math.min(distFromBeginningToBeginning, distFromBeginningToEnd)
                        curMinDistance = Math.min(curMinDistance, distFromEndToBeginning)
                        curMinDistance = Math.min(curMinDistance, distFromEndToEnd)
    
                        if (curMinDistance <= minDistanceToNewSegment) {
                            minDistanceToNewSegment = curMinDistance
    
                            when (curMinDistance) {
                                distFromBeginningToBeginning -> additionMode = ADDITION_MODE_BEGINNING_TO_BEGINNING
                                distFromBeginningToEnd -> additionMode = ADDITION_MODE_BEGINNING_TO_END
                                distFromEndToBeginning -> additionMode = ADDITION_MODE_END_TO_BEGINNING
                                distFromEndToEnd -> additionMode = ADDITION_MODE_END_TO_END
                            }
    
                            existingSegmentAsReference = existingSegment
                        }
                    }
    
                    addTrackSegment(existingSegmentAsReference, additionMode, newSegment, newTrackSegments)
                }
            }
            val sortedGeoJsonRoute = MultiLineString2("MultiLineString", newTrackSegments)
    
            val geoJsonRouteWriter = PrintWriter("src/main/assets/geojson/" + routeId + "-sorted.geojson")
            geoJsonRouteWriter.append(gson.toJson(sortedGeoJsonRoute))
            geoJsonRouteWriter.close()
        } catch (ex: ParserConfigurationException) { }
        catch (ex: SAXException) { }
        catch (ex: IOException) { }
        catch (ex: Exception) {
            print(ex.localizedMessage)
        }
    }
    
    private fun addTrackSegment(
        existingSegmentAsReference: ArrayList<ArrayList<Double>>?,
        additionMode: Int,
        newSegment: ArrayList<ArrayList<Double>>,
        newTrackSegments: ArrayList<ArrayList<ArrayList<Double>>>
    ) {
        if (existingSegmentAsReference != null) {
            when (additionMode) {
                ADDITION_MODE_BEGINNING_TO_BEGINNING -> {
                    val segmentToBeAdded = newSegment.reversed() as ArrayList<ArrayList<Double>>
                    val indexWhereToAddNewSegment = Math.max(0, newTrackSegments.indexOf(existingSegmentAsReference))
    
                    newTrackSegments.add(indexWhereToAddNewSegment, segmentToBeAdded)
                }
                ADDITION_MODE_BEGINNING_TO_END -> {
                    val indexWhereToAddNewSegment = Math.max(0, newTrackSegments.indexOf(existingSegmentAsReference))
    
                    newTrackSegments.add(indexWhereToAddNewSegment, newSegment)
                }
                ADDITION_MODE_END_TO_BEGINNING -> {
                    newTrackSegments.add(newSegment)
                }
                ADDITION_MODE_END_TO_END -> {
                    newTrackSegments.add(newSegment.reversed() as ArrayList<ArrayList<Double>>)
                }
            }
        }
    }
    
    fun distance(lat1: Double, lat2: Double, lon1: Double, lon2: Double): Double
    {
        val earthRadius = 6371
    
        val latDistance = Math.toRadians(lat2 - lat1)
        val lonDistance = Math.toRadians(lon2 - lon1)
        val a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + (Math.cos(Math.toRadians(lat1)) * Math.cos(
            Math.toRadians(lat2)
        ) * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2))
        val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
        val distance = earthRadius.toDouble() * c * 1000.0
    
        /* If you add el1 and el2 parameters, as elevation, then you coud make this change to take it in account for the distance. You'll have to remove, obviously, the return line that is now below this multiline comment
            val height = el1 - el2
            distance = Math.pow(distance, 2.0) + Math.pow(height, 2.0)
            return Math.sqrt(distance) */
    
        return Math.sqrt(Math.pow(distance, 2.0))
    }
    
xarlymg89
  • 2,552
  • 2
  • 27
  • 41