【问题标题】:How to sort a MultiLineString?如何对多行字符串进行排序?
【发布时间】:2018-10-10 23:58:06
【问题描述】:

我有一个 MultiLineString,它由形成路径的各个 LineStrings 组成。路径有一个方向,必须对 LineStrings 进行排序以反映此顺序。为此,必须将某些字符串反转以指向与其余字符串相同的方向。 什么是合适的算法来完成这项任务?

换句话说,对列表进行排序的最佳方法是什么,列表可以颠倒?即

输入:

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

输出:

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

【问题讨论】:

  • 这是 JavaScript 还是 Python 相关的?因为我不认为这两个标签都是必要的。

标签: javascript python geojson multilinestring


【解决方案1】:

Sorted() 带有列表理解

例如:

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

输出:

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

【讨论】:

  • 我猜我的例子不够清楚。由于输入是二维坐标列表,因此不能使用sorted。唯一可以应用于内部列表的操作是反向操作,算法应该决定是反向列表还是保持原样。
  • 对不起,我不明白。如果您需要反转,那么您可以尝试类似print(sorted([list(reversed(i)) for i in l]))
【解决方案2】:

未能找到解决方案,我最终编写了这个算法。它可以完成这项工作,但可以更好地处理分支,即。选择会产生最长连续路径的分支。现在它只是坚持第一条线段并从那里开始。

给定一个 GeoJSON MultiLineString 几何,该算法将线段排序为连续路径并返回一个新几何。

代码已根据 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

【讨论】:

    【解决方案3】:

    即使问题需要 Python 解决方案,并且由于我通过 DuckDuckGo 搜索试图找到解决此问题的方法(对 GeoJson MultiLineString 几何上的段进行排序)到达此页面,我认为 Java / Kotlin到达这里的其他人可能会赞赏解决方案。

    我最初想出了一个自己的解决方案,它恰好类似于@kissaprofeetta 的解决方案。虽然我有一些不同之处:

    • 我使用的距离算法更精确一些,旨在用于地理定位,因为它考虑到地球不是二维平面/地图,而是球体。事实上,高程数据也可以轻松添加,更加精确。
    • 将线段添加到新的 MultiLineString 数组的方式比 @kissaprofeetta 的回答要花哨一些
    • 我添加了 GeoJson 文件的读取和排序后 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))
      }
      

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-23
      • 2010-09-07
      • 2010-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多