Crie um arco de linhas a partir de uma linha e um valor

9

Estou tentando recriar um gráfico de origem-destino como este:

insira a descrição da imagem aqui

Consegui reunir os dados em uma tabela MSOA para LAD e posso desenhar um mapa como este para um dos MSOA de origem.

insira a descrição da imagem aqui

Uma vez que você permita as distâncias (agora ridículas) das pessoas no Peak District comutarem para o trabalho, está próximo.

Mas eu gosto bastante do efeito que o autor conseguiu ao "espalhar" as linhas. Obviamente, com fluxos de 522 e 371, não posso escolher uma única linha por passageiro, mas seria bom produzir um arco proporcional de linhas para mostrar o número de pessoas que fazem a viagem.

Eu pensei que seria capaz de usar o Geometry Generator, mas sem uma construção de loop, não consigo avançar.

Ian Turton
fonte
Essa ferramenta ESRI pode ser do seu interesse, ou pelo menos um trampolim para idéias de código sobre como criar uma "cunha" de linhas.
Hornbydd
E quando uma linha simboliza, digamos 50 (100, 200) passageiros, por linha? Com algum código python (ou o gerador de geometria, não tenho certeza), você pode girar as linhas (x / 50) com uma quantidade distinta.
Stefan

Respostas:

5

Um grande desafio!

Esta resposta usa principalmente o gerador de geometria e foi escrita no QGIS 3.2. O QGIS travou (sem eu ter salvo!) Logo depois que construí as linhas e quase desisti, mas a lista de expressões usadas recentemente salvou o dia - outro bônus ao usar o gerador de geometria

Comecei com dois conjuntos de pontos, uma fonte e três destinos. Os destinos são rotulados com as contagens:

Pontos iniciais

Em seguida, gerei linhas conectando o ponto de origem a todos os destinos usando uma camada virtual usando o seguinte código:

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

Pontos conectados

Em seguida, usei a seguinte expressão de gerador de geometria para estilizar as linhas:

 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))

Isso pega cada linha e aplica as seguintes etapas:

  1. Gera um buffer cônico que vai da largura zero na origem para uma largura dimensionada pela contagem de destino no final do destino. A densidade do ponto de buffer também é dimensionada pelo atributo de contagem de destino.
  2. Os vértices do polígono do buffer são convertidos em pontos (isso é possivelmente supérfluo) e depois exportados para o WKT, e os colchetes são removidos usando uma regex, antes de converter em uma matriz
  3. A matriz é então expandida de volta para uma sequência WKT para uma cadeia de linhas múltiplas, inserindo nas coordenadas do ponto de origem mais a formatação relevante - isso cria uma linha separada para cada um dos vértices extraídos conectados ao ponto de origem
  4. O WKT é convertido novamente em um objeto de geometria e finalmente interceptado com o buffer do ponto de origem para recortá-los no círculo em que o ponto de destino fica (consulte a saída de a tapered_bufferpara entender por que isso é necessário)

Fãs

Ao escrever as etapas, percebo que a conversão de e para uma matriz é desnecessária e toda a manipulação do WKT pode ser feita com expressões regulares. Esta expressão está abaixo e, se a tapered_arrayfunção puder ser substituída por uma diferente, também poderá ser usada no 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
fonte
6

Sua pergunta me deixou curiosa.

Esta solução funciona apenas para o QGIS 2.x no console Python

Como mencionado no meu comentário, aqui é minha ideia de criar o arco de linhas com o Python.

Eu tenho uma camada de dois pontos:

Eu. Um detentor do capital (id, capital)

ii. Um segurando as cidades (id, cidade, passageiros)

A quantidade de passageiros é "separada em notas" e estas serão as linhas que constroem o arco. Assim, 371 passageiros são uma combinação de 3x100, 1x50, 2x10 e 1x1 e no total 7 notas. Posteriormente, as linhas são estilizadas por um estilo baseado em regras.

Aqui está o 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()

O resultado pode ser assim:

insira a descrição da imagem aqui

ATUALIZAÇÃO: distinção masculino / feminino

Resultados em 4 camadas de memória.

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()

O resultado pode ser assim:insira a descrição da imagem aqui

Uma coisa que não é ideal do ponto de vista cartográfico:

O tamanho de um arco de linha pode ser irritante à primeira vista, da maneira que um arco maior pode representar mais passageiros. Um arco pode ser maior com menos passageiros (289 passageiros / 11 notas) do que outro com mais passageiros (311 passageiros / 5 notas).

Stefan
fonte