¿Cómo encajar una red de carreteras en una cuadrícula hexagonal en QGIS?

13

Estoy tratando de usar QGIS 2.14 para ajustar una red de carreteras a una cuadrícula hexagonal, pero obtengo artefactos extraños.

He creado una cuadrícula hexadecimal con MMQGIS , las celdas miden aproximadamente 20 x 23 m. He amortiguado la red de carreteras en 1 my la he densificado para que haya un nodo cada pocos metros. Puedes ver lo que estoy tratando de lograr a continuación. Como puede ver, puedo hacer que funcione en algunos casos:

  • azul es el camino densificado (una línea amortiguada)
  • rojo es la versión 'hexificada': esto es lo que quiero encontrar
  • el gris es la cuadrícula hexadecimal

ingrese la descripción de la imagen aquí

Luego utilicé la nueva función de geometrías de ajuste para ajustar los nodos a la esquina hexagonal más cercana. Los resultados son prometedores, pero parece haber algunos casos extremos en los que la línea se expande para llenar el hexágono (o parte de él):

ingrese la descripción de la imagen aquí

La razón del búfer es que las geometrías de ajuste no le permiten ajustar a una capa cuya geometría es diferente. Por ejemplo, no puede ajustar nodos en una capa LINE a puntos en una capa POINT). Parece ser más feliz al conectar POLYGON a POLYGON.

Sospecho que las carreteras se expanden cuando un lado de la línea de la carretera amortiguada salta a un lado de la celda hexagonal, y el otro lado salta al otro lado de la celda hexagonal. En mi ejemplo, las carreteras que cruzan oeste-este en un ángulo agudo parecen ser las peores.

Cosas que he intentado, sin éxito: -

  • amortiguando la red de carreteras en una pequeña cantidad, por lo que sigue siendo un polígono pero es muy delgada.
  • densificar las celdas hexadecimales (por lo que hay nodos a lo largo de los bordes, no solo en las esquinas)
  • variando la distancia máxima de ajuste (esto tiene el mayor efecto, pero parece que no puedo encontrar un valor ideal)
  • usando capas LINE, no POLYGONs

Me parece que si cambio a usar solo capas LINE, funciona por un tiempo, luego se bloquea. Parece guardar su trabajo a medida que avanza: algunas líneas se han procesado parcialmente.

ingrese la descripción de la imagen aquí

¿Alguien sabe de alguna otra manera de ajustar puntos en una línea al punto más cercano en otra línea / capa de polígono, idealmente sin necesidad de usar postgres / postgis (aunque una solución con postgis también sería bienvenida)?

EDITAR

Para cualquiera que quiera probar, he puesto un proyecto QGIS de inicio aquí en Dropbox . Esto incluye las capas Hex Grid y Densified lines. (La red de carreteras es de OSM, por lo que se puede descargar usando QuickOSM, por ejemplo, si necesita obtener el original para descomprimir las carreteras).

Tenga en cuenta que está en OSGB (epsg: 27700) que es un UTM localizado para el Reino Unido, con unidades en metros.

Steven Kay
fuente
3
¿Podría por favor compartir un conjunto de datos de muestra? Me gustaría intentarlo, pero no quiero repasar el proceso de creación de datos de muestra desde cero.
Germán Carrillo
@ GermánCarrillo - gracias. He agregado un enlace a un proyecto de muestra a la pregunta.
Steven Kay

Respuestas:

14

Mi solución involucra un script PyQGIS que es más rápido y más efectivo que un flujo de trabajo que involucra ajuste (también lo probé). Usando mi algoritmo obtuve estos resultados:

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Puede ejecutar los siguientes fragmentos de código en secuencia desde QGIS (en la consola QGIS Python). Al final obtienes una capa de memoria con las rutas ajustadas cargadas en QGIS.

El único requisito previo es crear un Shapefile de carretera multiparte (uso Processing->Singleparts to multipart, usé el campo fictitiuoscomo Unique ID fieldparámetro). Esto nos dará un roads_multipart.shparchivo con una sola característica.

Aquí está el algoritmo explicado:

  1. Obtenga los lados hexagonales más cercanos donde se cruzan las rutas. Para cada hexágono creamos 6 triángulos entre cada par de vértices vecinos y el centroide correspondiente. Si alguna carretera se cruza con un triángulo, el segmento compartido por el hexágono y el triángulo se agrega a la ruta ajustada final. Esta es la parte más pesada de todo el algoritmo, tarda 35 segundos en ejecutarse en mi máquina. En las dos primeras líneas hay 2 rutas de Shapefile, debe ajustarlas para que se ajusten a sus propias rutas de archivo.

    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. Deshágase de los segmentos desconectados (o 'abiertos') utilizando listas, tuplas y diccionarios de Python . En este punto, quedan algunos segmentos desconectados, es decir, segmentos que tienen un vértice desconectado pero el otro conectado a al menos otros 2 segmentos (ver segmentos rojos en la siguiente figura). Necesitamos deshacernos de ellos.

    ingrese la descripción de la imagen aquí

    # 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. Ahora podemos crear una capa vectorial a partir de la lista de coordenadas y cargarla en el mapa 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)

Otra parte del resultado:

ingrese la descripción de la imagen aquí

Si necesita atributos en las rutas ajustadas, podríamos usar un Índice espacial para evaluar rápidamente las intersecciones (como en /gis//a/130440/4972 ), pero esa es otra historia.

¡Espero que esto ayude!

Germán Carrillo
fuente
1
gracias, eso funciona perfectamente! Tuve problemas para pegarlo en la consola de Python ... Lo guardé como un archivo .py en el editor qgis python, y funcionó bien desde allí. El paso de varias partes elimina los atributos, ¡pero una unión de búfer / espacial lo arreglará!
Steven Kay
1
¡Excelente! Me alegra que finalmente haya resuelto el problema que estabas enfrentando Estoy interesado en saber cuál es el caso de uso con el que está tratando. ¿Crees que podríamos aprovechar esto para convertirnos en un complemento QGIS o tal vez un script que se incluye en los scripts de procesamiento?
Germán Carrillo
1
El caso de uso que tenía en mente eran los mapas de transporte público, como el Mapa del Tubo, en el que debe ajustar las líneas a una cuadrícula teselada o a un conjunto restringido de ángulos. Esto se puede hacer manualmente digitalizando, pero estaba interesado en ver si podría automatizarse. Usé hexágonos porque eran fáciles de generar, visualmente interesantes y tenían ángulos que no eran ángulos rectos. Creo que vale la pena verlo con más detalle, especialmente si podría generalizarse para trabajar con otras teselaciones ...
Steven Kay
1
La idea detrás del guión funcionaría en cuadrículas de triángulos, cuadrados, pentágonos, hexágonos, etc.
Germán Carrillo
6

Lo hice en ArcGIS, seguramente se puede implementar usando QGIS o simplemente python con un paquete capaz de leer geometrías. Asegúrese de que las carreteras representan la red, es decir, se cruzan entre sí solo en los extremos. Estás tratando con OSM, supongo que es el caso.

  • Convierta polígonos de proximidad en líneas y planérelos, para que también se conviertan en una red geométrica.
  • Coloque puntos en sus extremos - Puntos Voronoi: ingrese la descripción de la imagen aquí
  • Coloque puntos en la carretera a intervalos regulares de 5 m, asegúrese de que las carreteras de la red tengan un buen nombre único:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

  • Para cada punto de ruta, encuentre las coordenadas del punto Voronoi más cercano: ingrese la descripción de la imagen aquí
  • Cree "Caminos" conectando los puntos más cercanos en el mismo orden: ingrese la descripción de la imagen aquí

Si no quieres ver esto: ingrese la descripción de la imagen aquí

No intente usar puntos de PK en las líneas de Voronoi. Me temo que solo empeorará las cosas. Por lo tanto, su única opción es crear una red a partir de líneas Voronoi y encontrar rutas entre los puntos finales de la carretera, eso tampoco es gran cosa

FelixIP
fuente
¡Esto es genial, gracias! Menciona el uso de líneas voronoi, no demasiado familiarizado con eso (Voronois de puntos, puedo entender). ¿Quiere decir que cada línea está rodeada por un polígono de todos los puntos más cercanos a esa línea? (No conozco una forma de hacerlo en QGIS). ¿O te refieres a las líneas de límite de una malla voronoi normal, basada en puntos?
Steven Kay
Líneas de límite de polígonos de proximidad. Por cierto, me detuve demasiado temprano. Para completar la tarea, es suficiente dividir el primer resultado en el vértice, agregar un punto en el medio y repetir el proceso
FelixIP
4

Me doy cuenta de que estás pidiendo un método QGIS, pero ten paciencia para obtener una respuesta clara:

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

ingrese la descripción de la imagen aquí

Notas:

  • Este script contiene muchos bucles dentro de bucles y un cursor anidado. Definitivamente hay espacio para la optimización. Revisé sus conjuntos de datos en un par de minutos, pero más características agravarán el problema.
líber
fuente
Gracias por esto, muy apreciado. Esto muestra exactamente el efecto que estaba visualizando. Los abundantes comentarios significan que puedo entender lo que estás haciendo, incluso si no puedo ejecutar el código. Aunque es arcpy, estoy seguro de que esto será factible en pyqgis. Las ideas de algoritmos aquí son interesantes (especialmente mirando tanto en sentido horario como antihorario alrededor de cada hexágono, y eligiendo el camino más corto)
Steven Kay
2

Si dividiera la línea de la carretera en segmentos donde cada segmento estuviera completamente contenido por el hexágono, su decisión sobre qué segmentos de línea de hexágono usar sería si la distancia desde el centroide del segmento de carretera dividido hasta el punto medio de cada lado del hexágono era inferior a la mitad del diámetro del hexágono (o menor que el radio de un círculo que se ajusta dentro del hexágono).

Por lo tanto, si tuviera que (un segmento a la vez) seleccionar segmentos de línea de hexágono (donde cada segmento es un lado del hexágono) que están dentro de una distancia del radio del hexágono, podría copiar esas geometrías de línea y fusionarlas en cualquier identificador único que utilice para su conjunto de datos de carretera

Si tiene problemas para fusionarse con el identificador único, puede aplicar el búfer y seleccionar por ubicación solo en esos segmentos para aplicar los atributos de su conjunto de datos de carretera; de esa manera no tendría que preocuparse por hacer coincidencias falsas con un búfer que es demasiado grande.

El problema con la herramienta de ajuste es que ajusta puntos indiscriminadamente; Es difícil encontrar esa tolerancia perfecta para usar. Con esta metodología, estaría identificando correctamente qué segmentos de línea hexagonales usaría, luego reemplazaría la geometría de sus datos de carretera (o insertaría las geometrías en un conjunto de datos diferente).

Además, si todavía tiene el problema con los segmentos de línea que saltan de un lado del hexágono al otro, puede dividir la línea en segmentos por vértices, calcular la longitud de cada línea y luego eliminar cualquier segmento de línea que sea más grande que La longitud promedio de un lado del hexágono.

crld
fuente
1

El complemento de geometría en qgis 3.0 ha sido modificado y ahora permite el ajuste entre diferentes tipos de geometría. También tiene muchas correcciones. Puede probar una versión de "instantánea diaria" para obtener acceso al complemento mejorado antes de que se lance oficialmente 3.0.

ndawson
fuente