Как привязать дорожную сеть к шестиугольной сетке в QGIS?

13

Я пытаюсь использовать QGIS 2.14 для привязки дорожной сети к шестиугольной сетке, но у меня появляются странные артефакты.

Я создал шестнадцатеричную сетку с MMQGIS , ячейки размером около 20 х 23 м. Я забуферен дорожной сетью на 1 м и уплотнил ее, чтобы через каждые несколько метров находился узел. Вы можете увидеть то, что я пытаюсь достичь ниже. Как видите, я могу заставить его работать в некоторых случаях:

  • синий - уплотненная дорога (буферизованная линия)
  • красный - это «шестнадцатеричная» версия - это то, что я хочу найти
  • серый - это шестнадцатеричная сетка

введите описание изображения здесь

Затем я использовал новую функцию привязки геометрии, чтобы привязать узлы к ближайшему шестиугольному углу. Результаты многообещающие, но, похоже, существуют некоторые крайние случаи, когда линия расширяется, чтобы заполнить шестиугольник (или его часть):

введите описание изображения здесь

Причиной для буфера является то, что привязка геометрий не позволяет привязываться к слою, геометрия которого отличается. Например, вы не можете привязать узлы на слое LINE к точкам на слое POINT). Кажется, это самый счастливый привязать POLYGON к POLYGON.

Я подозреваю, что дороги расширяются, когда одна сторона буферизованной дорожной линии переходит на одну сторону гекс-ячейки, а другая - на другую сторону гекс-ячейки. В моем примере дороги, которые пересекают запад-восток под острым углом, кажутся худшими.

Вещи, которые я пытался без успеха: -

  • буферизует дорожную сеть незначительным количеством, поэтому она остается многоугольником, но очень тонкой.
  • уплотнение шестнадцатеричных ячеек (так что по краям есть узлы, а не только по углам)
  • изменение максимального расстояния привязки (это имеет самый большой эффект, но я не могу найти идеальное значение)
  • используя слои LINE, а не многоугольники

Я обнаружил, что если я перехожу к использованию только слоев LINE, он работает некоторое время, а затем падает. Кажется, он сохраняет свою работу, поскольку некоторые строки были частично обработаны.

введите описание изображения здесь

Кто-нибудь знает какой-либо другой способ привязки точек на линии к ближайшей точке на другом слое линий / полигонов, в идеале без необходимости использовать postgres / postgis (хотя решение с postgis также будет приветствоваться)?

РЕДАКТИРОВАТЬ

Для тех, кто хотел бы попробовать, я разместил стартовый проект QGIS здесь, на Dropbox . Это включает в себя слои Hex Grid и Densified lines. (Дорожная сеть основана на OSM, поэтому ее можно загрузить с помощью QuickOSM, например, если вам нужно получить оригинал для определения дороги).

Обратите внимание, что в OSGB (epsg: 27700) локализованный UTM для Великобритании с единицами измерения в метрах.

Стивен Кей
источник
3
Не могли бы вы поделиться примером набора данных? Я хотел бы попробовать, но не хочу проходить процесс создания образцов данных с нуля.
Герман Каррильо
@ GermánCarrillo - спасибо. Я добавил ссылку на пример проекта на вопрос.
Стивен Кей

Ответы:

14

Мое решение включает сценарий PyQGIS, который работает быстрее и эффективнее, чем рабочий процесс с привязкой (я тоже попробовал). Используя мой алгоритм, я получил эти результаты:

введите описание изображения здесь

введите описание изображения здесь

Вы можете запустить следующие фрагменты кода последовательно из QGIS (в консоли Python QGIS). В конце вы получите слой памяти с привязанными маршрутами, загруженными в QGIS.

Единственным предварительным условием является создание Shapefile из нескольких частей дороги (используйте Processing->Singleparts to multipart, я использовал поле в fictitiuosкачестве Unique ID fieldпараметра). Это даст нам roads_multipart.shpфайл с одной функцией.

Вот алгоритм объяснил:

  1. Получите ближайшие стороны шестиугольника, где пересекаются маршруты. Для каждого шестиугольника мы создаем 6 треугольников между каждой парой соседних вершин и соответствующим центроидом. Если какая-либо дорога пересекает треугольник, сегмент, разделенный шестиугольником и треугольником, добавляется к окончательному привязанному маршруту. Это более тяжелая часть всего алгоритма, она занимает 35 секунд на моей машине. В первых двух строках есть 2 пути Shapefile, вы должны настроить их так, чтобы они соответствовали вашим собственным путям к файлам.

    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 . На данный момент осталось несколько отсоединенных сегментов, то есть сегментов, у которых одна вершина отключена, а другая соединена, по крайней мере, с двумя другими сегментами (см. Красные сегменты на следующем рисунке). Мы должны избавиться от них.

    введите описание изображения здесь

    # 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 возникли проблемы ... Я сохранил его в виде файла .py в редакторе Python qgis, и с него все работало нормально. Шаг, состоящий из нескольких частей, удаляет атрибуты, но буферное / пространственное соединение исправит это!
Стивен Кей
1
Большой! Рад, что наконец решил проблему, с которой вы столкнулись. Мне интересно знать, с каким случаем использования вы имеете дело. Как вы думаете, мы могли бы использовать это, чтобы стать плагином QGIS или, возможно, сценарием, который включен в сценарии обработки?
Герман Каррильо
1
Я имел в виду вариант использования карт общественного транспорта, таких как Tube Map, где вам нужно привязать линии к тесселированной сетке или к ограниченному набору углов. Это можно сделать вручную путем оцифровки, но мне было интересно посмотреть, можно ли это автоматизировать. Я использовал гексы, поскольку их было легко генерировать, они были интересны визуально и имели углы, которые не были прямыми углами. Я думаю, что на это стоит обратить внимание более подробно, особенно если это можно обобщить для работы с другими тесселяциями ...
Стивен Кей
1
Идея сценария будет работать с сетками из треугольников, квадратов, пятиугольников, шестиугольников и так далее.
Герман Каррильо
6

Я сделал это в ArcGIS, конечно, может быть реализован с использованием QGIS или просто Python с пакетом, способным читать геометрии. Убедитесь, что дороги представляют собой сеть, т.е. пересекаются друг с другом только на концах. Вы имеете дело с OSM, я полагаю, это так.

  • Преобразуйте полигоны близости в линии и выровняйте их так, чтобы они также стали геометрической сетью.
  • Разместите точки на их концах - очки Вороного: введите описание изображения здесь
  • Размещайте точки на дороге с регулярным интервалом в 5 м, убедитесь, что у сетевых дорог есть хорошее уникальное имя:

введите описание изображения здесь введите описание изображения здесь

  • Для каждой дорожной точки найдите координаты ближайшей точки Вороного: введите описание изображения здесь
  • Создайте «Дороги», соединив ближайшие точки в том же порядке: введите описание изображения здесь

Если вы не хотите видеть это: введите описание изображения здесь

Не пытайтесь использовать цепные точки на Вороной. Боюсь, это только усугубит ситуацию. Таким образом, ваш единственный вариант - создать сеть из линий Вороного и найти маршруты между конечными точками дороги, что тоже не составляет особого труда.

FelixIP
источник
это здорово, спасибо! Вы упомянули использование вороной линии, не слишком знакомой с этим (Вороной из точек, я могу понять). Вы имеете в виду, что каждая линия окружена многоугольником всех точек, ближайших к этой линии? (Я не знаю, как это сделать в QGIS). Или вы имеете в виду граничные линии из нормального вороного меша, основанные на точках?
Стивен Кей
Граничные линии полигонов близости. Кстати, я остановился слишком рано. Чтобы выполнить задачу, достаточно разделить 1-й результат в вершине, добавить точку в середине и повторить процесс
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

введите описание изображения здесь

Примечания:

  • Этот скрипт содержит много циклов внутри циклов и вложенный курсор. Определенно есть место для оптимизации. Я проверил ваши наборы данных за пару минут, но дополнительные возможности усугубят проблему.
флоэма
источник
Спасибо за это, высоко ценится. Это показывает именно тот эффект, который я визуализировал. Обильные комментарии означают, что я могу понять суть того, что вы делаете, даже если я не могу запустить код. Несмотря на то, что это arcpy, я уверен, что это будет выполнимо в pyqgis. Идеи алгоритма здесь интересны (особенно если смотреть по часовой стрелке и против часовой стрелки вокруг каждого гекса, и выбирать кратчайший путь)
Стивен Кей
2

Если бы вы разделили линию дороги на сегменты, где каждый сегмент полностью содержался шестиугольником, ваше решение о том, какие сегменты линии шестиугольника использовать, будет зависеть от того, было ли расстояние от центроида сегмента дороги до средней точки каждой стороны шестиугольника меньше половины диаметр шестиугольника (или меньше радиуса круга, который входит в шестигранник).

Таким образом, если бы вы (по одному сегменту за раз) выбирали сегменты линий шестиугольника (где каждый сегмент является стороной шестиугольника), которые находятся на расстоянии от радиуса шестиугольника, вы можете скопировать эти геометрии линий и объединить их в любой уникальный идентификатор, который вы используете для своего набора данных дороги.

Если у вас возникли проблемы с объединением уникального идентификатора, вы можете применить буфер и выбрать по местоположению только те сегменты, чтобы применить атрибуты вашего набора данных дороги; таким образом, вам не придется беспокоиться о ложных совпадениях с слишком большим буфером.

Проблема с инструментом привязки заключается в том, что он фиксирует точки без разбора; Трудно найти идеальный допуск для использования. Используя эту методологию, вы будете правильно определять, какие сегменты линий шестиугольника использовать, а затем заменять геометрию ваших дорожных данных (или вставлять геометрии в другой набор данных).

Кроме того, если у вас все еще есть проблема с отрезками линий, которые перепрыгивают с одной стороны шестиугольника на другую, вы можете разбить линию на сегменты по вершинам, рассчитать длину каждой линии, а затем удалить все отрезки линии, которые больше, чем средняя длина одной стороны шестиугольника.

CRLD
источник
1

В qgis 3.0 был добавлен объект геометрии, теперь он позволяет привязываться к различным типам геометрии. В нем также есть много исправлений. Вы можете попробовать версию «ежедневного снимка», чтобы получить доступ к улучшенному snapper до официального выпуска 3.0.

ndawson
источник