19. Libreria analisi di rete

Suggerimento

I frammenti di codice di questa pagina necessitano delle seguenti importazioni se sei è al di fuori della console pyqgis:

from qgis.core import (
  QgsVectorLayer,
  QgsPointXY,
)

La libreria di analisi di rete può essere utilizzata per:

  • creare un grafico matematico a partire da dati geografici (layer vettoriali polilineari)

  • implementare i metodi di base della teoria dei grafi (attualmente solo l’algoritmo di Dijkstra)

La libreria di analisi di rete è stata creata esportando le funzioni di base dal plugin principale di RoadGraph e ora puoi utilizzare i suoi metodi nei plugin o direttamente dalla console Python.

19.1. Informazioni generali

In breve, un caso d’uso tipico può essere descritto come segue:

  1. creare un grafo da geodati (solitamente layer vettoriali polilineari)

  2. eseguire l’analisi del grafo

  3. utilizzare i risultati dell’analisi (ad esempio, visualizzarli)

19.2. Costruire un grafo

La prima cosa da fare è preparare i dati di input, cioè convertire un layer vettoriale in un grafo. Tutte le azioni successive utilizzeranno questo grafo, non il layer.

Come sorgente possiamo utilizzare qualsiasi layer vettoriale di polilinee. I nodi delle polilinee diventano vertici del grafo e i segmenti delle polilinee sono bordi del grafo. Se più nodi hanno le stesse coordinate, allora sono lo stesso vertice del grafo. Quindi due linee che hanno un nodo in comune diventano collegate tra loro.

Inoltre, durante la creazione del grafo è possibile «fissare» («legare») al layer del vettore di input un numero qualsiasi di punti aggiuntivi. Per ogni punto aggiuntivo verrà trovata una corrispondenza — con il vertice del grafo più vicino o con il bordo del grafo più vicino. In quest’ultimo caso, il bordo verrà diviso e verrà aggiunto un nuovo vertice.

Gli attributi del layer vettoriale e la lunghezza di un bordo possono essere utilizzati come proprietà di un bordo.

La conversione da un layer vettoriale a grafo viene effettuata utilizzando il modello di programmazione Builder. Un grafo viene costruito usando un cosiddetto Director. Per ora esiste un solo Director: QgsVectorLayerDirector. Il Director imposta le impostazioni di base che verranno utilizzate per costruire un grafico da un layer vettoriale di linee, utilizzato dal costruttore per creare il grafico. Attualmente, come nel caso del director, esiste un solo costruttore: QgsGraphBuilder, che crea oggetti QgsGraph. Puoi implementare i propri costruttori per creare un grafo compatibile con librerie come BGL o NetworkX.

Per calcolare le proprietà dei bordi si utilizza lo schema di programmazione strategy. Per ora sono disponibili solo le strategie QgsNetworkDistanceStrategy (che tiene conto della lunghezza del percorso) e QgsNetworkSpeedStrategy (che considera anche la velocità). Puoi implementare la tua strategia personale, che utilizzerà tutti i parametri necessari. Ad esempio, il plugin RoadGraph utilizza una strategia che calcola il tempo di percorrenza utilizzando la lunghezza dei bordi e il valore della velocità dagli attributi.

È il momento di addentrarsi nel processo.

Prima di tutto, per utilizzare questa libreria dobbiamo importare il modulo di analisi

from qgis.analysis import *

Poi alcuni esempi di creazione di un director

 1# don't use information about road direction from layer attributes,
 2# all roads are treated as two-way
 3director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
 4
 5# use field with index 5 as source of information about road direction.
 6# one-way roads with direct direction have attribute value "yes",
 7# one-way roads with reverse direction have the value "1", and accordingly
 8# bidirectional roads have "no". By default roads are treated as two-way.
 9# This scheme can be used with OpenStreetMap data
10director = QgsVectorLayerDirector(vectorLayer, 5, 'yes', '1', 'no', QgsVectorLayerDirector.DirectionBoth)

Per costruire un director, occorre passare un layer vettoriale che sarà usato come fonte per la struttura del grafo e le informazioni sul movimento consentito su ogni segmento stradale (movimento unidirezionale o bidirezionale, direzione diretta o inversa). La call si presenta come segue

1director = QgsVectorLayerDirector(vectorLayer,
2                                  directionFieldId,
3                                  directDirectionValue,
4                                  reverseDirectionValue,
5                                  bothDirectionValue,
6                                  defaultDirection)

Ecco l’elenco completo del significato di questi parametri:

  • vectorLayer — layer vettoriale usato per costruire il grafo

  • directionFieldId — indice del campo della tabella degli attributi, dove sono memorizzate le informazioni sulla direzione delle strade. Se -1, non utilizzare affatto questa informazione. Un numero intero.

  • directDirectionValue — Valore del campo per le strade con direzione diretta (che si muovono dal primo punto della linea all’ultimo). Una stringa.

  • reverseDirectionValue — valore del campo per le strade con direzione inversa (che si muovono dall’ultimo punto della linea al primo). Una stringa.

  • bothDirectionValue — valore del campo per le strade bidirezionali (per tali strade ci si può muovere dal primo punto all’ultimo e dall’ultimo al primo). Una stringa.

  • defaultDirection — direzione predefinita della strada. Questo valore sarà utilizzato per le strade in cui il campo directionFieldId non è impostato o ha un valore diverso da uno dei tre valori specificati sopra. I valori possibili sono:

È quindi necessario creare una strategia per il calcolo delle proprietà dei bordi

1# The index of the field that contains information about the edge speed
2attributeId = 1
3# Default speed value
4defaultValue = 50
5# Conversion from speed to metric units ('1' means no conversion)
6toMetricFactor = 1
7strategy = QgsNetworkSpeedStrategy(attributeId, defaultValue, toMetricFactor)

E informare il director di questa strategia

director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', 3)
director.addStrategy(strategy)

Ora possiamo usare il costruttore, che creerà il grafo. Il costruttore della classe QgsGraphBuilder utilizza diversi parametri:

  • crs — sistema di riferimento delle coordinate da usare. Parametro obbligatorio.

  • otfEnabled — utilizzare la riproiezione «al volo» o no. Per impostazione predefinita True (usa OTF).

  • topologyTolerance — tolleranza topologica. Il valore predefinito è 0.

  • ellipsoid — ellissoide da usare. Per impostazione predefinita «WGS84».

# only CRS is set, all other values are defaults
builder = QgsGraphBuilder(vectorLayer.crs())

Inoltre, possiamo definire diversi punti che verranno utilizzati nell’analisi. Ad esempio

startPoint = QgsPointXY(1179720.1871, 5419067.3507)
endPoint = QgsPointXY(1180616.0205, 5419745.7839)

Ora tutto è a posto e possiamo costruire il grafo e «legare» questi punti ad esso

tiedPoints = director.makeGraph(builder, [startPoint, endPoint])

La costruzione del grafo può richiedere un certo tempo (che dipende dal numero di elementi in un layer e dalla dimensione del layer). tiedPoints è un elenco con le coordinate dei punti «legati». Quando l’operazione di costruzione è terminata, si può ottenere il grafico e utilizzarlo per l’analisi

graph = builder.graph()

Con il codice che segue possiamo ottenere gli indici dei vertici dei nostri punti

startId = graph.findVertex(tiedPoints[0])
endId = graph.findVertex(tiedPoints[1])

19.3. Analisi grafo

L’analisi di rete viene utilizzata per trovare le risposte a due domande: quali vertici sono connessi e come trovare il percorso più breve. Per risolvere questi problemi, la libreria di analisi di rete fornisce l’algoritmo di Dijkstra.

L’algoritmo di Dijkstra trova il percorso più breve da un vertice del grafo a tutti gli altri e i valori dei parametri di ottimizzazione. I risultati possono essere rappresentati come un albero del percorso più breve.

L’albero del percorso più breve è un grafo diretto pesato (o più precisamente un albero) con le seguenti proprietà:

  • solo un vertice non ha bordi in entrata: la radice dell’albero.

  • tutti gli altri vertici hanno un solo bordo in entrata

  • se il vertice B è raggiungibile dal vertice A, allora il percorso da A a B è il solo percorso disponibile ed è ottimale (il più breve) su questo grafo

Per ottenere l’albero del percorso più breve, utilizzare i metodi shortestTree() e dijkstra() della classe QgsGraphAnalyzer. Si consiglia di utilizzare il metodo dijkstra() perché funziona più velocemente e utilizza la memoria in modo più efficiente.

Il metodo shortestTree() è utile quando si vuole percorrere l’albero del percorso più breve. Crea sempre un nuovo oggetto grafo (QgsGraph) e accetta tre variabili:

  • source — grafo in ingresso

  • startVertexIdx — indice del punto sull” albero (la radice dell’albero)

  • criterionNum — numero di proprietà del bordo da usare (a partire da 0).

tree = QgsGraphAnalyzer.shortestTree(graph, startId, 0)

Il metodo dijkstra() ha gli stessi argomenti, ma restituisce due array. Il primo elemento n contiene l’indice del bordo entrante o -1 se non ci sono bordi entranti. Nel secondo elemento n contiene la distanza dalla radice dell’albero al vertice n o DOUBLE_MAX se il vertice n non è raggiungibile dalla radice.

(tree, cost) = QgsGraphAnalyzer.dijkstra(graph, startId, 0)

Ecco un codice molto semplice per visualizzare l’albero del percorso più breve utilizzando il grafo creato con il metodo shortestTree() (seleziona il layer linestring nel pannello Layer e sostituisci le coordinate con le tue).

Avvertimento

Usa questo codice solo come esempio; crea molti oggetti QgsRubberBand e può essere lento su grandi insiemi di dati.

 1from qgis.core import *
 2from qgis.gui import *
 3from qgis.analysis import *
 4from qgis.PyQt.QtCore import *
 5from qgis.PyQt.QtGui import *
 6
 7vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
 8director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
 9strategy = QgsNetworkDistanceStrategy()
10director.addStrategy(strategy)
11builder = QgsGraphBuilder(vectorLayer.crs())
12
13pStart = QgsPointXY(1179661.925139,5419188.074362)
14tiedPoint = director.makeGraph(builder, [pStart])
15pStart = tiedPoint[0]
16
17graph = builder.graph()
18
19idStart = graph.findVertex(pStart)
20
21tree = QgsGraphAnalyzer.shortestTree(graph, idStart, 0)
22
23i = 0
24while (i < tree.edgeCount()):
25  rb = QgsRubberBand(iface.mapCanvas())
26  rb.setColor (Qt.red)
27  rb.addPoint (tree.vertex(tree.edge(i).fromVertex()).point())
28  rb.addPoint (tree.vertex(tree.edge(i).toVertex()).point())
29  i = i + 1

Stessa cosa, ma utilizzando il metodo dijkstra().

 1from qgis.core import *
 2from qgis.gui import *
 3from qgis.analysis import *
 4from qgis.PyQt.QtCore import *
 5from qgis.PyQt.QtGui import *
 6
 7vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
 8
 9director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
10strategy = QgsNetworkDistanceStrategy()
11director.addStrategy(strategy)
12builder = QgsGraphBuilder(vectorLayer.crs())
13
14pStart = QgsPointXY(1179661.925139,5419188.074362)
15tiedPoint = director.makeGraph(builder, [pStart])
16pStart = tiedPoint[0]
17
18graph = builder.graph()
19
20idStart = graph.findVertex(pStart)
21
22(tree, costs) = QgsGraphAnalyzer.dijkstra(graph, idStart, 0)
23
24for edgeId in tree:
25  if edgeId == -1:
26    continue
27  rb = QgsRubberBand(iface.mapCanvas())
28  rb.setColor (Qt.red)
29  rb.addPoint (graph.vertex(graph.edge(edgeId).fromVertex()).point())
30  rb.addPoint (graph.vertex(graph.edge(edgeId).toVertex()).point())

19.3.1. Trovare i percorsi più brevi

Per trovare il percorso ottimale tra due punti si utilizza il seguente approccio. Entrambi i punti (inizio A e fine B) sono «legati» al grafo quando viene costruito. Quindi, utilizzando il metodo shortestTree() o dijkstra() si costruisce l’albero del percorso più breve con radice nel punto iniziale A. Nello stesso albero si trova anche il punto finale B e si inizia a percorrere l’albero dal punto B al punto A. L’intero algoritmo può essere scritto come:

1assign T = B
2while T != B
3    add point T to path
4    get incoming edge for point T
5    look for point TT, that is start point of this edge
6    assign T = TT
7add point A to path

A questo punto abbiamo il percorso, sotto forma di elenco invertito di vertici (i vertici sono elencati in ordine inverso, dal punto finale al punto iniziale) che saranno visitati durante il percorso.

Ecco il codice di esempio per QGIS Python Console (potrebbe essere necessario caricare e selezionare un layer linestring in legenda e sostituire le coordinate nel codice con le tue) che utilizza il metodo shortestTree().

 1from qgis.core import *
 2from qgis.gui import *
 3from qgis.analysis import *
 4
 5from qgis.PyQt.QtCore import *
 6from qgis.PyQt.QtGui import *
 7
 8vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
 9builder = QgsGraphBuilder(vectorLayer.sourceCrs())
10director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
11
12startPoint = QgsPointXY(1179661.925139,5419188.074362)
13endPoint = QgsPointXY(1180942.970617,5420040.097560)
14
15tiedPoints = director.makeGraph(builder, [startPoint, endPoint])
16tStart, tStop = tiedPoints
17
18graph = builder.graph()
19idxStart = graph.findVertex(tStart)
20
21tree = QgsGraphAnalyzer.shortestTree(graph, idxStart, 0)
22
23idxStart = tree.findVertex(tStart)
24idxEnd = tree.findVertex(tStop)
25
26if idxEnd == -1:
27    raise Exception('No route!')
28
29# Add last point
30route = [tree.vertex(idxEnd).point()]
31
32# Iterate the graph
33while idxEnd != idxStart:
34    edgeIds = tree.vertex(idxEnd).incomingEdges()
35    if len(edgeIds) == 0:
36        break
37    edge = tree.edge(edgeIds[0])
38    route.insert(0, tree.vertex(edge.fromVertex()).point())
39    idxEnd = edge.fromVertex()
40
41# Display
42rb = QgsRubberBand(iface.mapCanvas())
43rb.setColor(Qt.green)
44
45# This may require coordinate transformation if project's CRS
46# is different than layer's CRS
47for p in route:
48    rb.addPoint(p)

Ed ecco lo stesso esempio, ma utilizzando il metodo dijkstra().

 1from qgis.core import *
 2from qgis.gui import *
 3from qgis.analysis import *
 4
 5from qgis.PyQt.QtCore import *
 6from qgis.PyQt.QtGui import *
 7
 8vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines')
 9director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
10strategy = QgsNetworkDistanceStrategy()
11director.addStrategy(strategy)
12
13builder = QgsGraphBuilder(vectorLayer.sourceCrs())
14
15startPoint = QgsPointXY(1179661.925139,5419188.074362)
16endPoint = QgsPointXY(1180942.970617,5420040.097560)
17
18tiedPoints = director.makeGraph(builder, [startPoint, endPoint])
19tStart, tStop = tiedPoints
20
21graph = builder.graph()
22idxStart = graph.findVertex(tStart)
23idxEnd = graph.findVertex(tStop)
24
25(tree, costs) = QgsGraphAnalyzer.dijkstra(graph, idxStart, 0)
26
27if tree[idxEnd] == -1:
28    raise Exception('No route!')
29
30# Total cost
31cost = costs[idxEnd]
32
33# Add last point
34route = [graph.vertex(idxEnd).point()]
35
36# Iterate the graph
37while idxEnd != idxStart:
38    idxEnd = graph.edge(tree[idxEnd]).fromVertex()
39    route.insert(0, graph.vertex(idxEnd).point())
40
41# Display
42rb = QgsRubberBand(iface.mapCanvas())
43rb.setColor(Qt.red)
44
45# This may require coordinate transformation if project's CRS
46# is different than layer's CRS
47for p in route:
48    rb.addPoint(p)

19.3.2. Zone di disponibilità

La zona di disponibilità per il vertice A è il sottoinsieme dei vertici del grafo che sono accessibili dal vertice A e il costo dei percorsi da A a questi vertici non è superiore a un certo valore.

Questo può essere mostrato più chiaramente con il seguente esempio: «C’è una stazione dei vigili del fuoco. Quali zone della città può raggiungere un camion dei pompieri in 5 minuti? 10 minuti? 15 minuti?». Le risposte a queste domande sono le zone di disponibilità della caserma dei pompieri.

Per trovare le zone di disponibilità possiamo usare il metodo dijkstra() della classe QgsGraphAnalyzer. È sufficiente confrontare gli elementi dell’array cost con un valore predefinito. Se cost[i] è minore o uguale a un valore predefinito, allora il vertice i è all’interno della zona di disponibilità, altrimenti è fuori.

Un problema più difficile è quello di ottenere i confini della zona di disponibilità. Il confine inferiore è l’insieme dei vertici ancora accessibili, mentre il confine superiore è l’insieme dei vertici non accessibili. In realtà è semplice: si tratta del confine di disponibilità basato sui bordi dell’albero del cammino più breve per i quali il vertice di origine del bordo è accessibile e il vertice di destinazione del bordo no.

Ecco un esempio

 1director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth)
 2strategy = QgsNetworkDistanceStrategy()
 3director.addStrategy(strategy)
 4builder = QgsGraphBuilder(vectorLayer.crs())
 5
 6
 7pStart = QgsPointXY(1179661.925139, 5419188.074362)
 8delta = iface.mapCanvas().getCoordinateTransform().mapUnitsPerPixel() * 1
 9
10rb = QgsRubberBand(iface.mapCanvas(), True)
11rb.setColor(Qt.green)
12rb.addPoint(QgsPointXY(pStart.x() - delta, pStart.y() - delta))
13rb.addPoint(QgsPointXY(pStart.x() + delta, pStart.y() - delta))
14rb.addPoint(QgsPointXY(pStart.x() + delta, pStart.y() + delta))
15rb.addPoint(QgsPointXY(pStart.x() - delta, pStart.y() + delta))
16
17tiedPoints = director.makeGraph(builder, [pStart])
18graph = builder.graph()
19tStart = tiedPoints[0]
20
21idStart = graph.findVertex(tStart)
22
23(tree, cost) = QgsGraphAnalyzer.dijkstra(graph, idStart, 0)
24
25upperBound = []
26r = 1500.0
27i = 0
28tree.reverse()
29
30while i < len(cost):
31    if cost[i] > r and tree[i] != -1:
32        outVertexId = graph.edge(tree [i]).toVertex()
33        if cost[outVertexId] < r:
34            upperBound.append(i)
35    i = i + 1
36
37for i in upperBound:
38    centerPoint = graph.vertex(i).point()
39    rb = QgsRubberBand(iface.mapCanvas(), True)
40    rb.setColor(Qt.red)
41    rb.addPoint(QgsPointXY(centerPoint.x() - delta, centerPoint.y() - delta))
42    rb.addPoint(QgsPointXY(centerPoint.x() + delta, centerPoint.y() - delta))
43    rb.addPoint(QgsPointXY(centerPoint.x() + delta, centerPoint.y() + delta))
44    rb.addPoint(QgsPointXY(centerPoint.x() - delta, centerPoint.y() + delta))