Come agganciare una rete stradale a una griglia esagonale in QGIS?


13

Sto cercando di utilizzare QGIS 2.14 per agganciare una rete stradale a una griglia esagonale, ma sto ottenendo strani artefatti.

Ho creato una griglia esadecimale con MMQGIS , le celle misurano circa 20 x 23 m. Ho bufferizzato la rete stradale di 1 metro e l' ho densificata in modo che ci sia un nodo ogni pochi metri. Di seguito puoi vedere cosa sto cercando di ottenere. Come puoi vedere, posso farlo funzionare in alcuni casi: -

  • blu è la strada densificata (una linea tamponata)
  • il rosso è la versione "esadecimale" - questo è quello che voglio trovare
  • il grigio è la griglia esadecimale

inserisci qui la descrizione dell'immagine

Ho quindi usato la nuova funzione Geometrie snap per agganciare i nodi all'angolo esagonale più vicino. I risultati sono promettenti, ma sembrano esserci alcuni casi limite in cui la linea si espande per riempire l'esagono (o parte di esso): -

inserisci qui la descrizione dell'immagine

Il motivo del buffer è che le geometrie Snap non consentono di eseguire lo snap su un livello la cui geometria è diversa. Ad esempio, non è possibile agganciare i nodi su un livello LINE ai punti su un livello POINT). Sembra essere il più felice che fa scattare POLYGON in POLYGON.

Sospetto che le strade si espandano quando un lato della linea tamponata salta su un lato della cella esadecimale e l'altro lato salta sull'altro lato della cella esadecimale. Nel mio esempio, le strade che attraversano ovest-est ad angolo acuto sembrano essere le peggiori.

Cose che ho provato, senza successo: -

  • buffering della rete stradale di una piccola quantità, quindi rimane un poligono ma è molto sottile.
  • densificare le celle esadecimali (quindi ci sono nodi lungo i bordi, non solo agli angoli)
  • variando la distanza massima di scatto (questo ha l'effetto più grande, ma non riesco a trovare un valore ideale)
  • usando i livelli LINE, non POLYGONs

Trovo che se cambio a usare solo i livelli LINE, funziona per un po ', quindi si blocca. Sembra salvare il suo lavoro mentre procede - alcune linee sono state parzialmente elaborate.

inserisci qui la descrizione dell'immagine

Qualcuno conosce un altro modo per agganciare punti su una linea al punto più vicino su un'altra linea / strato poligonale, idealmente senza la necessità di usare Postgres / Postgis (anche se sarebbe gradita anche una soluzione con Postgis)?

MODIFICARE

Per chiunque volesse provare, ho messo un progetto QGIS di partenza qui su Dropbox . Ciò include i livelli Griglia esadecimale e Linee densificate. (La rete stradale proviene da OSM, quindi può essere scaricata utilizzando QuickOSM, ad esempio se è necessario ottenere l'originale per non compensare le strade).

Si noti che è in OSGB (epsg: 27700) che un UTM localizzato per il Regno Unito, con unità in metri.


3
Potresti condividere un set di dati di esempio? Mi piacerebbe provare, ma non voglio andare oltre il processo di creazione di dati di esempio da zero.
Germán Carrillo,

@ GermánCarrillo - grazie. Ho aggiunto un link a un progetto di esempio alla domanda.
Steven Kay,

Risposte:


14

La mia soluzione prevede uno script PyQGIS che è più veloce ed efficace di un flusso di lavoro che coinvolge lo snap (l'ho provato anche io). Usando il mio algoritmo ho ottenuto questi risultati:

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

È possibile eseguire i seguenti frammenti di codice in sequenza da QGIS (nella console di QGIS Python). Alla fine ottieni un livello di memoria con i percorsi spezzati caricati in QGIS.

L'unico prerequisito è la creazione di uno Shapefile stradale multiparte (uso Processing->Singleparts to multipart, ho utilizzato il campo fictitiuoscome Unique ID fieldparametro). Questo ci darà un roads_multipart.shpfile con una singola funzione.

Ecco l'algoritmo spiegato:

  1. Ottieni i lati esagonali più vicini dove incrociano le rotte. Per ogni esagono creiamo 6 triangoli tra ogni coppia di vertici vicini e il centroide corrispondente. Se una strada interseca un triangolo, il segmento condiviso dall'esagono e il triangolo viene aggiunto al percorso di aggancio finale. Questa è la parte più pesante dell'intero algoritmo, ci vogliono 35 secondi in esecuzione sulla mia macchina. Nelle prime due righe ci sono 2 percorsi Shapefile, dovresti regolarli per adattarli ai tuoi percorsi file.

    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. Sbarazzati di segmenti disconnessi (o "aperti") usando elenchi, tuple e dizionari di Python . A questo punto, ci sono alcuni segmenti disconnessi rimasti, cioè segmenti che hanno un vertice disconnesso ma l'altro collegato ad almeno altri 2 segmenti (vedere i segmenti rossi nella figura successiva). Dobbiamo liberarcene.

    inserisci qui la descrizione dell'immagine

    # 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. Ora possiamo creare un livello vettoriale dall'elenco delle coordinate e caricarlo sulla mappa 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)

Un'altra parte del risultato:

inserisci qui la descrizione dell'immagine

Se avessi bisogno di attributi nei percorsi spezzati, potremmo usare un indice spaziale per valutare rapidamente le intersezioni (come in /gis//a/130440/4972 ), ma questa è un'altra storia.

Spero che sia di aiuto!


1
grazie, funziona perfettamente! Ho avuto problemi a incollarlo nella console di Python ... L'ho salvato come file .py nell'editor python di qgis, e da lì è andato tutto bene. Il passo multipart rimuove gli attributi, ma un buffer / join spaziale lo risolverà!
Steven Kay,

1
Grande! Lieto che alla fine abbia risolto il problema che stavi affrontando. Sono interessato a sapere qual è il caso d'uso con cui hai a che fare. Pensi che potremmo sfruttare questo per diventare un plug-in QGIS o forse uno script incluso negli script di elaborazione?
Germán Carrillo,

1
il caso d'uso che avevo in mente erano le mappe dei trasporti pubblici come la Tube Map, in cui è necessario agganciare le linee a una griglia tassellata o ad una serie ristretta di angoli. Questo può essere fatto manualmente digitalizzando, ma ero interessato a vedere se poteva essere automatizzato. Ho usato gli esagoni poiché erano facili da generare, visivamente interessanti e avevano angoli che non erano retti. Penso che valga la pena esaminarlo in modo più dettagliato, soprattutto se si potesse generalizzare per lavorare con altre tessellazioni ...
Steven Kay,

1
L'idea alla base della sceneggiatura avrebbe funzionato su griglie di triangoli, quadrati, pentagoni, esagoni e così via.
Germán Carrillo,

6

L'ho fatto in ArcGIS, sicuramente può essere implementato usando QGIS o semplicemente python con un pacchetto in grado di leggere le geometrie. Assicurati che le strade rappresentino la rete, cioè si intersecano solo alle estremità. Hai a che fare con OSM, suppongo che sia il caso.

  • Converti i poligoni di prossimità in linee e planarizzali, in modo che diventino anche una rete geometrica.
  • Posiziona i punti alle loro estremità - Punti Voronoi: inserisci qui la descrizione dell'immagine
  • Posiziona i punti sulla strada a intervalli regolari di 5 m, assicurati che le strade della rete abbiano un buon nome univoco:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

  • Per ogni Road Point trovare le coordinate del punto Voronoi più vicino: inserisci qui la descrizione dell'immagine
  • Crea "Strade" collegando i punti più vicini nello stesso ordine: inserisci qui la descrizione dell'immagine

Se non vuoi vedere questo: inserisci qui la descrizione dell'immagine

Non tentare di utilizzare i punti di catena sulla Voronoi Lines. Temo che peggiorerà le cose. Quindi la tua unica opzione è quella di creare una rete da linee Voronoi e trovare percorsi tra i punti finali della strada, anche questo non è un grosso problema


è fantastico, grazie! Lei menziona l'uso di linee di voronoi, non troppo familiari (Voronois da punti, posso capire). Vuoi dire che ogni linea è circondata da un poligono di tutti i punti più vicini a quella linea? (Non sono a conoscenza di un modo per farlo in QGIS). O intendi le linee di confine di una normale rete voronoi, in base ai punti?
Steven Kay,

Linee di confine di poligoni di prossimità. A proposito, mi sono fermato troppo presto. Per completare l'attività è sufficiente dividere il 1 ° risultato al vertice, aggiungere il punto nel mezzo e ripetere il processo
FelixIP

4

Mi rendo conto che stai chiedendo un metodo QGIS, ma abbi pazienza con me per una risposta arcpia:

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

inserisci qui la descrizione dell'immagine

Appunti:

  • Questo script contiene molti loop all'interno di loop e un cursore nidificato. C'è sicuramente spazio per l'ottimizzazione. Ho esaminato i tuoi set di dati in un paio di minuti, ma più funzioni aggraveranno il problema.

Grazie per questo, molto apprezzato. Questo mostra esattamente l'effetto che stavo visualizzando. I copiosi commenti significano che posso avere un'idea di ciò che stai facendo anche se non riesco a eseguire il codice. Sebbene sia arcpico, sono sicuro che questo sarà fattibile in Pyqgis. Le idee dell'algoritmo qui sono interessanti (soprattutto guardando sia in senso orario che antiorario attorno ad ogni esagono, e scegliendo la via più breve)
Steven Kay

2

Se dovessi dividere la linea stradale in segmenti in cui ciascun segmento era completamente contenuto dall'esagono, la tua decisione su quali segmenti di linea esagonale usare sarebbe se la distanza dal centroide del segmento di strada diviso al punto medio di ciascun lato dell'esagono fosse inferiore alla metà del diametro dell'esagono (o inferiore al raggio di un cerchio che si inserisce all'interno dell'esagono).

Quindi, se dovessi (un segmento alla volta) selezionare segmenti di linea esagonale (dove ogni segmento è un lato dell'esagono) che si trovano entro una distanza dal raggio dell'esagono, potresti copiare quelle geometrie di linea e fonderle su qualunque identificatore univoco usi per il tuo set di dati stradali.

In caso di problemi di fusione con l'identificatore univoco, è possibile applicare il buffer e selezionare per posizione solo su quei segmenti per applicare gli attributi del set di dati della strada; in questo modo non dovresti preoccuparti di fare false corrispondenze con un buffer troppo grande.

Il problema con lo strumento snap è che cattura i punti in modo indiscriminato; è difficile trovare quella tolleranza perfetta da usare. Con questa metodologia identificheresti correttamente quali segmenti di linea esagonale utilizzare, quindi sostituendo la geometria dei dati della tua strada (o inserendo le geometrie in un set di dati diverso).

Inoltre, se hai ancora il problema con i segmenti di linea che saltano da un lato dell'esagono all'altro, puoi dividere la linea in segmenti per vertici, calcolare la lunghezza di ogni linea, quindi rimuovere tutti i segmenti di linea più grandi di la lunghezza media di un lato dell'esagono.


1

Lo snapper della geometria in qgis 3.0 è stato rielaborato e ora consente lo snap tra diversi tipi di geometria. Ha anche molte correzioni. È possibile provare una versione "istantanea giornaliera" per ottenere l'accesso allo snapper migliorato prima che 3.0 sia ufficialmente rilasciato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.