Crear un arco de líneas a partir de una línea y un valor.

9

Estoy tratando de recrear una trama Origen-Destino como esta:

ingrese la descripción de la imagen aquí

He logrado combinar los datos en una tabla MSOA a LAD y puedo dibujar un mapa como este para uno de los MSOA de origen.

ingrese la descripción de la imagen aquí

Lo que una vez que permite las distancias (ahora ridículas) que las personas en el Peak District viajan al trabajo está cerca.

Pero me gusta bastante el efecto que el autor ha logrado al "extender" las líneas. Obviamente, con flujos de 522 y 371, no puedo ir por una sola línea por viajero, pero sería bueno producir un arco proporcional de líneas para mostrar la cantidad de personas que hacen el viaje.

Pensé que sería capaz de usar el Geometry Generator, pero sin una construcción de bucle, parece que no puedo avanzar.

Ian Turton
fuente
Esta herramienta de ESRI puede ser de su interés, o al menos un trampolín para ideas de código para crear una "cuña" de líneas.
Hornbydd
Y cuando una línea simboliza, digamos 50 (100, 200) viajeros, por línea? Con algún código de Python (o el generador de geometría, no estoy seguro) podría rotar las líneas (x / 50) con una cantidad distinta.
Stefan

Respuestas:

5

¡Un gran desafío!

Esta respuesta utiliza principalmente el generador de geometría y se escribió en QGIS 3.2. QGIS se bloqueó (¡sin que yo haya guardado!) Justo después de construir las líneas por primera vez y casi me doy por vencido, pero la lista de expresiones utilizada recientemente salvó el día, otra ventaja de usar el generador de Geometría

Comencé con dos conjuntos de puntos, una fuente y tres destinos. Los destinos están etiquetados con los recuentos:

Puntos iniciales

Luego generé líneas que conectan el punto de origen a todos los destinos usando una capa virtual usando el siguiente código:

SELECT d.Count_MF, Makeline( s.geometry, d.geometry) 'geometry' 
  FROM Source AS s JOIN Destinations AS d

Puntos conectados

Luego utilicé la siguiente expresión del generador de Geometría para diseñar las líneas:

 intersection(
   geom_from_wkt( 
     'MULTILINESTRING ((' ||  $x_at( 0)  || ' ' || $y_at( 0)  || ', ' || 
     array_to_string(
       array_remove_at( string_to_array( regexp_replace(
             geom_to_wkt(nodes_to_points( tapered_buffer(  $geometry ,0, "Count_MF" * 200, floor("Count_MF" / 10)),true)),
             '[\\(\\)]','')),0)
     , ') , ('  ||  $x_at( 0)  || ' ' || $y_at( 0)  || ', ' )
    || '))')
    ,buffer( point_n(  $geometry ,1), $length))

Esto toma cada línea y aplica los siguientes pasos:

  1. Genera un búfer cónico que va de ancho cero en el origen a un ancho escalado por el recuento de destino en el extremo de destino. La densidad del punto de amortiguación también se escala por el atributo de recuento de destino.
  2. Los vértices del polígono del búfer se convierten en puntos (esto es posiblemente superfluo), y luego se exportan a WKT, y los corchetes se eliminan usando una expresión regular, antes de convertirlos en una matriz
  3. La matriz se expande nuevamente a una cadena WKT para una multilínea, insertando en las coordenadas del punto fuente más el formato relevante; esto crea una línea separada para cada uno de los vértices extraídos conectados al punto fuente
  4. El WKT se convierte de nuevo en un objeto de geometría y finalmente se cruza con el búfer del punto de origen para recortarlos nuevamente en el círculo en el que se encuentra el punto de destino (vea la salida de a tapered_bufferpara comprender por qué esto es necesario)

Aficionados

Al escribir los pasos, me doy cuenta de que la conversión hacia y desde una matriz no es necesaria, y toda la manipulación de WKT se puede hacer con expresiones regulares. Esta expresión está debajo, y si la tapered_arrayfunción puede ser reemplazada por otra diferente, esto también podría usarse en QGIS 2.18.

intersection(
   geom_from_wkt(
    'MULTILINESTRING ((' ||  $x_at( 0)  || ' ' || $y_at( 0)  || ', ' ||
  replace(
    regexp_replace(
      regexp_replace(
        geom_to_wkt(tapered_buffer(  $geometry ,0, "Count_MF" * 200, floor("Count_MF" / 10))),
      '^[^,]*,',''),
    ',[^,]*$',''),
  ',',') , ('  ||  $x_at( 0)  || ' ' || $y_at( 0)  || ', ')
  || '))')
,buffer( point_n(  $geometry ,1), $length))
Andy Harfoot
fuente
6

Tu pregunta me hizo curioso.

Esta solución solo funciona para QGIS 2.x en la consola de Python

Como mencioné en mi comentario aquí, es mi idea crear el arco de líneas con Python.

Tengo dos capas de puntos:

yo. Uno que tiene la capital (id, capital)

ii. Uno que tiene las ciudades (id, ciudad, viajeros)

La cantidad de viajeros se "separa en billetes" y estas serán las líneas que construirán el arco. Entonces, 371 viajeros son una combinación de 3x100, 1x50, 2x10 y 1x1 y en total 7 billetes. Posteriormente, las líneas se diseñan con un estilo basado en reglas.

Aquí está el código:

from qgis.gui import *
from qgis.utils import *
from qgis.core import *
from PyQt4 import QtGui, uic
from PyQt4.QtGui import *
from PyQt4.QtCore import *

for lyr in QgsMapLayerRegistry.instance().mapLayers().values():
    if lyr.name() == "capital":
        capital_layer = lyr

for lyr in QgsMapLayerRegistry.instance().mapLayers().values():
    if lyr.name() == "town":
        town_layer = lyr

    # creating the memory layer
d_lyr = QgsVectorLayer('LineString', 'distance', 'memory')
QgsMapLayerRegistry.instance().addMapLayer(d_lyr)
prov = d_lyr.dataProvider()
prov.addAttributes( [ QgsField("id", QVariant.Int), QgsField("banknote",QVariant.Int)])

    # function to create the banknotes
def banknoteOutput(number):
    number_list = []
    number_list.append(number)
    banknote_count = []
    temp_list = []
    banknote_list = []
    for n in number_list:
        total_sum = 0
        total = int(n/100)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 100])
        n = n-(total*100)
        total = int(n/50)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 50])
        n = n-(total*50)
        total = int(n/10)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 10])
        n = n-(total*10)
        total = int(n/5)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 5])
        n = n-(total*5)
        total = int(n/1)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 1])
        for i in banknote_count:
            temp_list.append(i*i[0])
        banknote_list = [item for sublist in temp_list for item in sublist][1::2]
        return banknote_list

        # creating lines with the amount of banknotes
for capital in capital_layer.getFeatures():
    for town in town_layer.getFeatures():
        commuter_splitting = banknoteOutput(town['commuters'])
        for i,banknote in enumerate(commuter_splitting):
            angle = 2
            distance = QgsDistanceArea()
            distance.measureLine(capital.geometry().asPoint(), town.geometry().asPoint())
            vect = QgsFeature()
            vect.setGeometry(QgsGeometry.fromPolyline([capital.geometry().asPoint(), town.geometry().asPoint()]))
            vect.geometry().rotate(0+(i*angle), capital.geometry().asPoint())
            vect.setAttributes([int(town["id"]), int(banknote)])
            prov.addFeatures([vect])

d_lyr.updateExtents()
d_lyr.triggerRepaint()
d_lyr.updateFields()

El resultado podría verse así:

ingrese la descripción de la imagen aquí

ACTUALIZACIÓN: distinción hombre / mujer

Resultados en 4 capas de memoria.

from qgis.gui import *
from qgis.utils import *
from qgis.core import *
from PyQt4 import QtGui, uic
from PyQt4.QtGui import *
from PyQt4.QtCore import *

for lyr in QgsMapLayerRegistry.instance().mapLayers().values():
    if lyr.name() == "capital":
        capital_layer = lyr

for lyr in QgsMapLayerRegistry.instance().mapLayers().values():
    if lyr.name() == "town":
        town_layer = lyr

    # function to create the banknotes
def banknoteOutput(number):
    number_list = []
    number_list.append(number)
    banknote_count = []
    temp_list = []
    banknote_list = []
    for n in number_list:
        total_sum = 0
        total = int(n/100)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 100])
        n = n-(total*100)
        total = int(n/50)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 50])
        n = n-(total*50)
        total = int(n/10)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 10])
        n = n-(total*10)
        total = int(n/5)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 5])
        n = n-(total*5)
        total = int(n/1)
        total_sum = total_sum + total
        if total > 0:
            banknote_count.append([total, 1])
        for i in banknote_count:
            temp_list.append(i*i[0])
        banknote_list = [item for sublist in temp_list for item in sublist][1::2]
        return banknote_list

    # creating the male memory layer
cmt_male = QgsVectorLayer('LineString', 'Commuters_Male', 'memory')
QgsMapLayerRegistry.instance().addMapLayer(cmt_male)
prov_male = cmt_male.dataProvider()
prov_male.addAttributes( [ QgsField("id", QVariant.Int), QgsField("banknote",QVariant.Int)])

    # creating the male polygon memory layer
cmt_male_polygon = QgsVectorLayer('Polygon', 'Commuters_Male_Poly', 'memory')
QgsMapLayerRegistry.instance().addMapLayer(cmt_male_polygon)
prov_cmt_male_polygon = cmt_male_polygon.dataProvider()
prov_cmt_male_polygon.addAttributes( [ QgsField("id", QVariant.Int), QgsField("banknote",QVariant.Int)])

    # creating lines with the amount of banknotes
for capital in capital_layer.getFeatures():
    for town in town_layer.getFeatures():
        commuter_splitting = banknoteOutput(town['cmt_male'])
        points = []
        for i,banknote in enumerate(reversed(commuter_splitting)):
            angle = 2
            distance = QgsDistanceArea()
            distance.measureLine(capital.geometry().asPoint(), town.geometry().asPoint())
            vect = QgsFeature()
            vect.setGeometry(QgsGeometry.fromPolyline([capital.geometry().asPoint(), town.geometry().asPoint()]))
            vect.geometry().rotate(0+(i*angle), capital.geometry().asPoint())
            vect.setAttributes([int(town["id"]), int(banknote)])
            points.append(vect.geometry().asPolyline()[1])
            prov_male.addFeatures([vect])
        polygon = QgsFeature()
        points.insert(0,capital.geometry().asPoint())
        points.insert(len(points),capital.geometry().asPoint())
        polygon.setGeometry(QgsGeometry.fromPolygon([points]))
        polygon.setAttributes([1, 2])
        prov_cmt_male_polygon.addFeatures([polygon])

cmt_male.updateExtents()
cmt_male.triggerRepaint()
cmt_male.updateFields()
cmt_male_polygon.updateExtents()
cmt_male_polygon.triggerRepaint()
cmt_male_polygon.updateFields()

    # creating the female memory layer
cmt_female = QgsVectorLayer('LineString', 'Commuters_Female', 'memory')
QgsMapLayerRegistry.instance().addMapLayer(cmt_female)
prov_female = cmt_female.dataProvider()
prov_female.addAttributes( [ QgsField("id", QVariant.Int), QgsField("banknote",QVariant.Int)])

    # creating the female polygon memory layer
cmt_female_polygon = QgsVectorLayer('Polygon', 'Commuters_Female_Poly', 'memory')
QgsMapLayerRegistry.instance().addMapLayer(cmt_female_polygon)
prov_cmt_female_polygon = cmt_female_polygon.dataProvider()
prov_cmt_female_polygon.addAttributes( [ QgsField("id", QVariant.Int), QgsField("banknote",QVariant.Int)])

    # creating lines with the amount of banknotes
for capital in capital_layer.getFeatures():
    for town in town_layer.getFeatures():
        commuter_splitting = banknoteOutput(town['cmt_female'])
        points = []
        for i,banknote in enumerate(commuter_splitting):
            angle = 2
            distance = QgsDistanceArea()
            distance.measureLine(capital.geometry().asPoint(), town.geometry().asPoint())
            vect = QgsFeature()
            vect.setGeometry(QgsGeometry.fromPolyline([capital.geometry().asPoint(), town.geometry().asPoint()]))
            vect.geometry().rotate(-angle-(i*angle), capital.geometry().asPoint())
            vect.setAttributes([int(town["id"]), int(banknote)])
            points.append(vect.geometry().asPolyline()[1])
            prov_female.addFeatures([vect])
        polygon = QgsFeature()
        points.insert(0,capital.geometry().asPoint())
        points.insert(len(points),capital.geometry().asPoint())
        polygon.setGeometry(QgsGeometry.fromPolygon([points]))
        polygon.setAttributes([1, 2])
        prov_cmt_female_polygon.addFeatures([polygon])

cmt_female.updateExtents()
cmt_female.triggerRepaint()
cmt_female.updateFields()
cmt_female_polygon.updateExtents()
cmt_female_polygon.triggerRepaint()
cmt_female_polygon.updateFields()

El resultado podría verse así:ingrese la descripción de la imagen aquí

Una cosa que no es ideal desde el punto de vista cartográfico:

El tamaño de un arco de línea puede ser irritante a primera vista, en la forma en que un arco más grande podría representar a más viajeros. Un arco puede ser más grande con menos viajeros (289 viajeros / 11 billetes) que otro con más viajeros (311 viajeros / 5 billetes).

Stefan
fuente