道路ネットワークをQGISの六角形グリッドにスナップする方法は?


13

QGIS 2.14を使用して道路網を六角形のグリッドにスナップしようとしていますが、奇妙なアーティファクトが発生しています。

MMQGISで六角グリッドを作成しました。セルは約20 x 23 mです。道路ネットワークを1mバッファリングし、数メートルごとにノードがあるように高密度化しました。私が達成しようとしていることを以下で見ることができます。あなたが見ることができるように、私はいくつかのケースでそれを動作させることができます:-

  • 青は緻密化された道路(緩衝線)
  • 赤は「hexified」バージョンです-これは私が見つけたいものです
  • 灰色は六角形のグリッドです

ここに画像の説明を入力してください

次に、新しいスナップジオメトリ機能を使用して、ノードを最も近い六角形の角にスナップしました。結果は有望ですが、ラインが六角形(またはその一部)を埋めるために広がるいくつかのエッジケースがあるようです:

ここに画像の説明を入力してください

バッファの理由は、ジオメトリのスナップではジオメトリが異なるレイヤーにスナップできないためです。たとえば、LINEレイヤーのノードをPOINTレイヤーのポイントにスナップすることはできません)。POLYGONをPOLYGONにスナップするのが一番幸せそうです。

バッファリングされた道路線の片側がヘックスセルの片側にジャンプし、反対側がヘックスセルの反対側にジャンプすると、道路が広がると思います。私の例では、鋭角に西から東に交差する道路は最悪のようです。

私が試したもの、成功なし:-

  • 道路ネットワークをわずかにバッファリングするため、ポリゴンのままですが、非常に薄いです。
  • 六角セルの密度を高める(したがって、コーナーだけでなくエッジに沿ってノードがある)
  • 最大スナップ距離の変更(これは最大の効果をもたらしますが、理想的な値を見つけることができないようです)
  • POLYGONではなくLINEレイヤーを使用

LINEレイヤーのみを使用するように変更すると、しばらく機能し、その後クラッシュすることがわかりました。作業をそのまま保存しているようです-一部の行は部分的に処理されています。

ここに画像の説明を入力してください

理想的にはpostgres / postgisを使用する必要なしに、ライン上のポイントを別のライン/ポリゴンレイヤー上の最も近いポイントにスナップする他の方法を知っていますか(postgisを使用したソリューションも歓迎します)。

編集

試してみたい人のために、ここでDropboxにスターターQGISプロジェクトを配置しました。これには、Hex GridおよびDensified Linesレイヤーが含まれます。(道路ネットワークはOSMからのものであるため、QuickOSMを使用してダウンロードすることができます。たとえば、道路を高密度化するためにオリジナルを取得する必要がある場合)。

OSGB(epsg:27700)であり、単位はメートルで、英国向けにローカライズされたUTMであることに注意してください。


3
サンプルデータセットを共有してもらえますか?試してみたいのですが、サンプルデータを最初から作成するプロセスをやり直したくありません。
ジェルマンカリージョ

@GermánCarrillo-ありがとう。サンプルプロジェクトへのリンクを質問に追加しました。
スティーブンケイ

回答:


14

私のソリューションには、スナップを伴うワークフローよりも高速で効果的なPyQGISスクリプトが含まれています(私も試してみました)。私のアルゴリズムを使用して、私はこれらの結果を得ました:

ここに画像の説明を入力してください

ここに画像の説明を入力してください

次のコードスニペットをQGIS内(QGIS Pythonコンソール内)から順番に実行できます。最後に、スナップされたルートがQGISにロードされたメモリレイヤーを取得します。

唯一の前提条件は、マルチパート道路のシェープファイルを作成することです(Processing->Singleparts to multipartフィールドfictitiuosUnique ID fieldパラメーターとして使用しました)。これroads_multipart.shpにより、単一の機能を持つファイルが作成されます。

以下にアルゴリズムを説明します。

  1. ルートが交差する最も近い六角形の辺を取得します。六角形ごとに、隣接する頂点の各ペアと対応する重心の間に6つの三角形を作成します。道路が三角形と交差する場合、六角形と三角形が共有するセグメントが最終的なスナップルートに追加されます。これはアルゴリズム全体の中で重い部分であり、私のマシンで実行するには35秒かかります。最初の2行には2つのシェープファイルパスがありますが、独自のファイルパスに合わせて調整する必要があります。

    hexgrid = QgsVectorLayer("/docs/borrar/hex_grid_question/layers/normal-hexgrid.shp", "hexgrid", "ogr")
    roads = QgsVectorLayer("/docs/borrar/hex_grid_question/layers/roads_multipart.shp", "roads", "ogr")  # Must be multipart!
    
    roadFeat = roads.getFeatures().next() # We just have 1 geometry
    road = roadFeat.geometry() 
    indicesHexSides = ((0,1), (1,2), (2,3), (3,4), (4,5), (5,0))
    
    epsilon = 0.01
    # Function to compare whether 2 segments are equal (even if inverted)
    def isSegmentAlreadySaved(v1, v2):
        for segment in listSegments:        
            p1 = QgsPoint(segment[0][0], segment[0][1])
            p2 = QgsPoint(segment[1][0], segment[1][1])
            if v1.compare(p1, epsilon) and v2.compare(p2, epsilon) \
                or v1.compare(p2, epsilon) and v2.compare(p1, epsilon):
                return True
        return False
    
    # Let's find the nearest sides of hexagons where routes cross
    listSegments = []
    for hexFeat in hexgrid.getFeatures():
        hex = hexFeat.geometry()
        if hex.intersects( road ):
            for side in indicesHexSides:
                triangle = QgsGeometry.fromPolyline([hex.centroid().asPoint(), hex.vertexAt(side[0]), hex.vertexAt(side[1])])
                if triangle.intersects( road ):
                    # Only append new lines, we don't want duplicates!!!
                    if not isSegmentAlreadySaved(hex.vertexAt(side[0]), hex.vertexAt(side[1])): 
                        listSegments.append( [[hex.vertexAt(side[0]).x(), hex.vertexAt(side[0]).y()], [hex.vertexAt(side[1]).x(),hex.vertexAt(side[1]).y()]] )  
  2. Pythonリスト、タプル、およびディクショナリを使用して、切断された(または「オープン」)セグメントを取り除きます。この時点で、いくつかの切断されたセグメントが残っています。つまり、1つの頂点が切断され、もう1つが少なくとも他の2つのセグメントに接続されているセグメントです(次の図の赤いセグメントを参照)。それらを取り除く必要があります。

    ここに画像の説明を入力してください

    # Let's remove disconnected/open segments
    lstVertices = [tuple(point) for segment in listSegments for point in segment]
    dictConnectionsPerVertex = dict((tuple(x),lstVertices.count(x)-1) for x in set(lstVertices))
    
    # A vertex is not connected and the other one is connected to 2 segments
    def segmentIsOpen(segment):
        return dictConnectionsPerVertex[tuple(segment[0])] == 0 and dictConnectionsPerVertex[tuple(segment[1])] >= 2 \
            or dictConnectionsPerVertex[tuple(segment[1])] == 0 and dictConnectionsPerVertex[tuple(segment[0])] >= 2
    
    # Remove open segments
    segmentsToDelete = [segment for segment in listSegments if segmentIsOpen(segment)]        
    for toBeDeleted in segmentsToDelete:
        listSegments.remove( toBeDeleted )
  3. これで、座標のリストからベクターレイヤー作成し、QGISマップにロードできます

    # Create a memory layer and load it to QGIS map canvas
    vl = QgsVectorLayer("LineString", "Snapped Routes", "memory")
    pr = vl.dataProvider()
    features = []
    for segment in listSegments:
        fet = QgsFeature()
        fet.setGeometry( QgsGeometry.fromPolyline( [QgsPoint(segment[0][0], segment[0][1]), QgsPoint(segment[1][0], segment[1][1])] ) )
        features.append(fet)
    
    pr.addFeatures( features )
    vl.updateExtents()
    QgsMapLayerRegistry.instance().addMapLayer(vl)

結果の別の部分:

ここに画像の説明を入力してください

スナップされたルートに属性が必要な場合、空間インデックスを使用して交差点をすばやく評価できます(/gis//a/130440/4972など)が、それは別の話です。

お役に立てれば!


1
ありがとう、それは完璧に機能します!pythonコンソールへの貼り付けに問題がありました... qgis pythonエディターで.pyファイルとして保存すると、そこから正常に実行されました。マルチパートのステップは属性を削除しますが、バッファー/空間結合により修正されます!
スティーブンケイ

1
すごい!あなたが直面していた問題を最終的に解決してくれてうれしいです。私はあなたが扱っているユースケースが何であるかを知りたいです。これを活用して、QGISプラグイン、またはおそらくProcessingスクリプトに含まれるスクリプトになると思いますか?
ゲルマンカリージョ

1
私が念頭に置いたユースケースは、チューブマップのような公共交通機関のマップでした。このマップでは、ラインをモザイク状のグリッドまたは制限された角度のセットにスナップする必要があります。これはデジタル化することで手動で行うことができますが、自動化できるかどうか興味がありました。簡単に生成でき、視覚的に面白く、角度が直角ではないヘックスを使用しました。私は...それは他のtesselationsで動作するように一般化することができた場合は特に、これは、より詳細に見て価値があると思う
スティーブン・ケイ

1
スクリプトの背後にあるアイデアは、三角形、正方形、五角形、六角形などのグリッドで機能します。
ゲルマンカリージョ

6

私はArcGISでそれをしました。確かにQGISを使用して実装するか、単にジオメトリを読み取ることができるパッケージを備えたpythonで実装することができます。道路がネットワークを表していること、つまり、端でのみ互いに​​交差していることを確認してください。あなたはOSMを扱っています、私はそうだと思います。

  • 近接ポリゴンをラインに変換して平面化すると、ジオメトリックネットワークにもなります。
  • 両端にポイントを配置–ボロノイポイント: ここに画像の説明を入力してください
  • 5 mの規則的な間隔で道路上にポイントを配置し、ネットワーク道路に適切な一意の名前があることを確認します。

ここに画像の説明を入力してください ここに画像の説明を入力してください

  • すべてのロードポイントについて、最も近いボロノイポイントの座標を見つけます。 ここに画像の説明を入力してください
  • 同じ順序で最も近いポイントを接続して「道路」を作成します。 ここに画像の説明を入力してください

これを見たくない場合: ここに画像の説明を入力してください

ボロノイ線上のチェーンポイントを使用しようとしないでください。悪化するだけだと思います。したがって、あなたの唯一のオプションは、ボロノイ線からネットワークを作成し、道路の終点の間のルートを見つけることです。


これは素晴らしい、ありがとう!ボロノイ線を使用することについて言及しますが、それについてはあまり詳しくありません(ポイントからのボロノワ、理解できます)。各ラインは、そのラインに最も近いすべてのポイントのポリゴンで囲まれているということですか?(私はQGISでそれを行う方法を知りません)。または、ポイントに基づいて、通常のボロノイメッシュからの境界線を意味しますか?
スティーブンケイ

近接ポリゴンの境界線。ところで私は早すぎる停止しました。タスクを完了するには、頂点で最初の結果を分割し、中間にポイントを追加してプロセスを繰り返すだけで十分
です-FelixIP

4

私はあなたがQGISメソッドを求めていることを理解していますが、奇妙な答えを私に負ってください:

roads = 'clipped roads' # roads layer
hexgrid = 'normal-hexgrid' # hex grid layer
sr = arcpy.Describe('roads').spatialReference # spatial reference
outlines = [] # final output lines
points = [] # participating grid vertices
vert_dict = {} # vertex dictionary
hex_dict = {} # grid dictionary
with arcpy.da.SearchCursor(roads,["SHAPE@","OID@"], spatial_reference=sr) as r_cursor: # loop through roads
    for r_row in r_cursor:
        with arcpy.da.SearchCursor(hexgrid,["SHAPE@","OID@"], spatial_reference=sr) as h_cursor: # loop through hex grid
            for h_row in h_cursor:
                if not r_row[0].disjoint(h_row[0]): # check if the shapes overlap
                    hex_verts = []
                    for part in h_row[0]:
                        for pnt in part:
                            hex_verts.append(pnt) # add grid vertices to list
                    int_pts = r_row[0].intersect(h_row[0],1) # find all intersection points between road and grid
                    hex_bnd = h_row[0].boundary() # convert grid to line
                    hex_dict[h_row[1]] = hex_bnd # add grid geometry to dictionary
                    for int_pt in int_pts: # loop through intersection points
                        near_dist = 1000 # arbitrary large number
                        int_pt = arcpy.PointGeometry(int_pt,sr)
                        for hex_vert in hex_verts: # loop through hex vertices
                            if int_pt.distanceTo(hex_vert) < near_dist: # find shortest distance between intersection point and grid vertex
                                near_vert = hex_vert # remember geometry
                                near_dist = int_pt.distanceTo(hex_vert) # remember distance
                        vert_dict.setdefault(h_row[1],[]).append(arcpy.PointGeometry(near_vert,sr)) # store geometry in dictionary
                        points.append(arcpy.PointGeometry(near_vert,sr)) # add to points list
for k,v in vert_dict.iteritems(): # loop through participating vertices
    if len(v) < 2: # skip if there was only one vertex
        continue
    hex = hex_dict[k] # get hex grid geometry
    best_path = hex # longest line possible is hex grid boundary
    for part in hex:
        for int_vert in v: # loop through participating vertices
            for i,pnt in enumerate(part): # loop through hex grid vertices
                if pnt.equals(int_vert): # find vertex index on hex grid corresponding to current point
                    start_i = i
                    if start_i == 6:
                        start_i = 0
                    for dir in [[0,6,1],[5,-1,-1]]: # going to loop once clockwise, once counter-clockwise
                        past_pts = 0 # keep track of number of passed participating vertices
                        cur_line_arr = arcpy.Array() # polyline coordinate holder
                        cur_line_arr.add(part[start_i]) # add starting vertex to growing polyline
                        for j in range(dir[0],dir[1],dir[2]): # loop through hex grid vertices
                            if past_pts < len(v): # only make polyline until all participating vertices have been visited
                                if dir[2] == 1: # hex grid vertex index bookkeeping
                                    if start_i + j < 6:
                                        index = start_i + j
                                    else:
                                        index = (start_i - 6) + j
                                else:
                                    index = j - (5 - start_i)
                                    if index < 0:
                                        index += 6
                                cur_line_arr.add(part[index]) # add current vertex to growing polyline
                                for cur_pnt in v:
                                    if part[index].equals(cur_pnt): # check if the current vertex is a participating vertex
                                        past_pts += 1 # add to counter
                        if cur_line_arr.count > 1:
                            cur_line = arcpy.Polyline(cur_line_arr,sr)
                            if cur_line.length < best_path.length: # see if current polyline is shorter than any previous candidate
                                best_path = cur_line # if so, store polyline
    outlines.append(best_path) # add best polyline to list
arcpy.CopyFeatures_management(outlines, r'in_memory\outlines') # write list
arcpy.CopyFeatures_management(points, r'in_memory\mypoints') # write points, if you want

ここに画像の説明を入力してください

ノート:

  • このスクリプトには、ループ内の多くのループとネストされたカーソルが含まれています。最適化の余地は間違いなくあります。数分でデータセットを調べましたが、より多くの機能が問題を悪化させます。

これをありがとう、感謝します。これは、私が視覚化した効果を正確に示しています。豊富なコメントは、コードを実行できなくても、あなたが何をしているのかを把握できることを意味します。これは難解ですが、これはpyqgisで実行できると確信しています。ここでのアルゴリズムのアイデアは興味深いです(特に、各ヘックスの周りに時計回りと反時計回りの両方を見て、最短経路を選択します)
スティーブンケイ

2

道路線を各セグメントが六角形に完全に含まれるセグメントに分割する場合、使用する六角形線セグメントの決定は、分割された道路セグメントの重心から各六角形側面の中間点までの距離が半分未満かどうかです六角形の直径(または六角形の内側に収まる円の半径未満)。

したがって、六角形の半径の距離内にある六角形線セグメント(各セグメントが六角形の辺である)を(一度に1セグメント)選択する場合、それらの線形状をコピーしてそれらを結合できます道路データセットに使用する一意の識別子。

一意の識別子のマージに問題がある場合は、バッファを適用し、それらのセグメントのみの場所で選択して道路データセットの属性を適用できます。そうすれば、バッファが大きすぎると誤ってマッチすることを心配する必要がなくなります。

スナップツールの問題は、ポイントを無差別にスナップすることです。使用するための完璧な許容範囲を見つけることは困難です。この方法論を使用すると、使用する六角形の線セグメントを正しく識別し、道路データのジオメトリを置き換えます(またはジオメトリを別のデータセットに挿入します)。

また、六角形の一方の側から他方の側にジャンプするラインセグメントにまだ問題がある場合は、ラインを頂点ごとにセグメントに分割し、各ラインの長さを計算してから、六角形の一辺の平均長。


1

qgis 3.0のジオメトリスナッパーが作り直され、異なるジオメトリタイプ間でスナップできるようになりました。また、多くの修正があります。3.0が正式にリリースされる前に、「毎日のスナップショット」バージョンを試して、改善されたスナッパーにアクセスできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.