From c0b253a69bfc465ca3aa565b2b362a7a100884dd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Nov 2021 10:41:56 +1000 Subject: [PATCH] Add API to remove vertices or edges from QgsGraph Useful when you've built a graph and want to perform multiple different analysis on it after excluding routes without having to rebuild the whole graph again Eg. find the shortest path between two vertices, then remove all these edge from this path and repeat to try to find the second-shortest path which doesn't use any of the same edges from the shortest path --- .../auto_generated/network/qgsgraph.sip.in | 51 +++++ src/analysis/network/qgsgraph.cpp | 47 +++++ src/analysis/network/qgsgraph.h | 74 +++++++ tests/src/python/test_qgsgraph.py | 185 ++++++++++++++++++ 4 files changed, 357 insertions(+) diff --git a/python/analysis/auto_generated/network/qgsgraph.sip.in b/python/analysis/auto_generated/network/qgsgraph.sip.in index e2b5949ac0e..e52f36702e1 100644 --- a/python/analysis/auto_generated/network/qgsgraph.sip.in +++ b/python/analysis/auto_generated/network/qgsgraph.sip.in @@ -161,6 +161,30 @@ Returns the vertex at the given index. } %End + + void removeVertex( int index ) const; +%Docstring +Removes the vertex at specified ``index``. + +All edges which are incoming or outgoing edges for the vertex will also be removed. + +:raises IndexError: if the vertex is not found. + +.. versionadded:: 3.24 +%End +%MethodCode + auto it = sipCpp->mGraphVertices.constFind( a0 ); + if ( it != sipCpp->mGraphVertices.constEnd() ) + { + sipCpp->removeVertex( a0 ); + } + else + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } +%End + int edgeCount() const; %Docstring Returns number of graph edges @@ -186,6 +210,33 @@ Returns the edge at the given index. } %End + + + void removeEdge( int index ) const; +%Docstring +Removes the edge at specified ``index``. + +The incoming and outgoing edges for all graph vertices will be updated accordingly. Vertices which +no longer have any incoming or outgoing edges as a result will be removed from the graph automatically. + +:raises IndexError: if the vertex is not found. + +.. versionadded:: 3.24 +%End +%MethodCode + auto it = sipCpp->mGraphEdges.constFind( a0 ); + if ( it != sipCpp->mGraphEdges.constEnd() ) + { + sipCpp->removeEdge( a0 ); + } + else + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } +%End + + int findVertex( const QgsPointXY &pt ) const; %Docstring Find vertex by associated point diff --git a/src/analysis/network/qgsgraph.cpp b/src/analysis/network/qgsgraph.cpp index 37443d9f254..5f7941c5560 100644 --- a/src/analysis/network/qgsgraph.cpp +++ b/src/analysis/network/qgsgraph.cpp @@ -19,6 +19,7 @@ */ #include "qgsgraph.h" +#include int QgsGraph::addVertex( const QgsPointXY &pt ) { @@ -51,6 +52,24 @@ const QgsGraphVertex &QgsGraph::vertex( int idx ) const Q_ASSERT_X( false, "QgsGraph::vertex()", "Invalid vertex ID" ); } +void QgsGraph::removeVertex( int index ) +{ + auto it = mGraphVertices.constFind( index ); + if ( it != mGraphVertices.constEnd() ) + { + QSet< int > affectedEdges = qgis::listToSet( it->incomingEdges() ); + affectedEdges.unite( qgis::listToSet( it->outgoingEdges() ) ); + + mGraphVertices.erase( it ); + + // remove affected edges + for ( int edgeId : std::as_const( affectedEdges ) ) + { + mGraphEdges.remove( edgeId ); + } + } +} + const QgsGraphEdge &QgsGraph::edge( int idx ) const { auto it = mGraphEdges.constFind( idx ); @@ -59,6 +78,34 @@ const QgsGraphEdge &QgsGraph::edge( int idx ) const Q_ASSERT_X( false, "QgsGraph::edge()", "Invalid edge ID" ); } +void QgsGraph::removeEdge( int index ) +{ + auto it = mGraphEdges.constFind( index ); + if ( it != mGraphEdges.constEnd() ) + { + const int fromVertex = it->fromVertex(); + const int toVertex = it->toVertex(); + mGraphEdges.erase( it ); + + // clean up affected vertices + auto vertexIt = mGraphVertices.find( fromVertex ); + if ( vertexIt != mGraphVertices.end() ) + { + vertexIt->mOutgoingEdges.removeAll( index ); + if ( vertexIt->mOutgoingEdges.empty() && vertexIt->mIncomingEdges.empty() ) + mGraphVertices.erase( vertexIt ); + } + + vertexIt = mGraphVertices.find( toVertex ); + if ( vertexIt != mGraphVertices.end() ) + { + vertexIt->mIncomingEdges.removeAll( index ); + if ( vertexIt->mOutgoingEdges.empty() && vertexIt->mIncomingEdges.empty() ) + mGraphVertices.erase( vertexIt ); + } + } +} + int QgsGraph::vertexCount() const { return mGraphVertices.size(); diff --git a/src/analysis/network/qgsgraph.h b/src/analysis/network/qgsgraph.h index a6d220ac173..dc6b0791716 100644 --- a/src/analysis/network/qgsgraph.h +++ b/src/analysis/network/qgsgraph.h @@ -193,6 +193,41 @@ class ANALYSIS_EXPORT QgsGraph % End #endif +#ifndef SIP_RUN + + /** + * Removes the vertex at specified \a index. + * + * All edges which are incoming or outgoing edges for the vertex will also be removed. + * + * \since QGIS 3.24 + */ + void removeVertex( int index ); +#else + + /** + * Removes the vertex at specified \a index. + * + * All edges which are incoming or outgoing edges for the vertex will also be removed. + * + * \throws IndexError if the vertex is not found. + * \since QGIS 3.24 + */ + void removeVertex( int index ) const; + % MethodCode + auto it = sipCpp->mGraphVertices.constFind( a0 ); + if ( it != sipCpp->mGraphVertices.constEnd() ) + { + sipCpp->removeVertex( a0 ); + } + else + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + % End +#endif + /** * Returns number of graph edges */ @@ -226,6 +261,45 @@ class ANALYSIS_EXPORT QgsGraph % End #endif + +#ifndef SIP_RUN + + /** + * Removes the edge at specified \a index. + * + * The incoming and outgoing edges for all graph vertices will be updated accordingly. Vertices which + * no longer have any incoming or outgoing edges as a result will be removed from the graph automatically. + * + * \since QGIS 3.24 + */ + void removeEdge( int index ); +#else + + /** + * Removes the edge at specified \a index. + * + * The incoming and outgoing edges for all graph vertices will be updated accordingly. Vertices which + * no longer have any incoming or outgoing edges as a result will be removed from the graph automatically. + * + * \throws IndexError if the vertex is not found. + * \since QGIS 3.24 + */ + void removeEdge( int index ) const; + % MethodCode + auto it = sipCpp->mGraphEdges.constFind( a0 ); + if ( it != sipCpp->mGraphEdges.constEnd() ) + { + sipCpp->removeEdge( a0 ); + } + else + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + % End +#endif + + /** * Find vertex by associated point * \returns vertex index diff --git a/tests/src/python/test_qgsgraph.py b/tests/src/python/test_qgsgraph.py index 5da56b62add..650fd4adfe2 100644 --- a/tests/src/python/test_qgsgraph.py +++ b/tests/src/python/test_qgsgraph.py @@ -121,6 +121,191 @@ class TestQgsGraph(unittest.TestCase): with self.assertRaises(IndexError): graph.edge(3) + def test_remove_vertex(self): + graph = QgsGraph() + + with self.assertRaises(IndexError): + graph.removeVertex(0) + with self.assertRaises(IndexError): + graph.removeVertex(-1) + + v1 = graph.addVertex(QgsPointXY(1, 1)) + v2 = graph.addVertex(QgsPointXY(2, 2)) + v3 = graph.addVertex(QgsPointXY(3, 3)) + v4 = graph.addVertex(QgsPointXY(4, 4)) + edge_1 = graph.addEdge(v1, v2, [1]) + edge_2 = graph.addEdge(v2, v1, [1]) + edge_3 = graph.addEdge(v2, v3, [1]) + edge_4 = graph.addEdge(v2, v4, [1]) + edge_5 = graph.addEdge(v3, v4, [1]) + + self.assertEqual(graph.vertexCount(), 4) + self.assertEqual(graph.edgeCount(), 5) + + with self.assertRaises(IndexError): + graph.removeVertex(5) + + # remove a vertex + graph.removeVertex(v3) + self.assertEqual(graph.vertexCount(), 3) + with self.assertRaises(IndexError): + graph.vertex(v3) + self.assertEqual(graph.edgeCount(), 3) + self.assertEqual(graph.edge(edge_1).fromVertex(), v1) + self.assertEqual(graph.edge(edge_2).fromVertex(), v2) + self.assertEqual(graph.edge(edge_4).fromVertex(), v2) + + # edges 3 and 5 must be removed + with self.assertRaises(IndexError): + graph.edge(edge_3) + with self.assertRaises(IndexError): + graph.edge(edge_5) + + with self.assertRaises(IndexError): + graph.removeVertex(v3) + + # remove another vertex + graph.removeVertex(v1) + self.assertEqual(graph.vertexCount(), 2) + with self.assertRaises(IndexError): + graph.vertex(v1) + self.assertEqual(graph.edgeCount(), 1) + self.assertEqual(graph.edge(edge_4).fromVertex(), v2) + with self.assertRaises(IndexError): + graph.edge(edge_1) + with self.assertRaises(IndexError): + graph.edge(edge_2) + with self.assertRaises(IndexError): + graph.edge(edge_3) + with self.assertRaises(IndexError): + graph.edge(edge_5) + + with self.assertRaises(IndexError): + graph.removeVertex(v1) + + # remove another vertex + graph.removeVertex(v4) + self.assertEqual(graph.vertexCount(), 1) + with self.assertRaises(IndexError): + graph.vertex(v4) + self.assertEqual(graph.edgeCount(), 0) + with self.assertRaises(IndexError): + graph.edge(edge_1) + with self.assertRaises(IndexError): + graph.edge(edge_2) + with self.assertRaises(IndexError): + graph.edge(edge_3) + with self.assertRaises(IndexError): + graph.edge(edge_4) + with self.assertRaises(IndexError): + graph.edge(edge_5) + + with self.assertRaises(IndexError): + graph.removeVertex(v4) + + # remove last vertex + graph.removeVertex(v2) + self.assertEqual(graph.vertexCount(), 0) + self.assertEqual(graph.edgeCount(), 0) + with self.assertRaises(IndexError): + graph.vertex(v2) + + with self.assertRaises(IndexError): + graph.removeVertex(v2) + + def test_remove_edge(self): + graph = QgsGraph() + + with self.assertRaises(IndexError): + graph.removeEdge(0) + with self.assertRaises(IndexError): + graph.removeEdge(-1) + + v1 = graph.addVertex(QgsPointXY(1, 1)) + v2 = graph.addVertex(QgsPointXY(2, 2)) + v3 = graph.addVertex(QgsPointXY(3, 3)) + v4 = graph.addVertex(QgsPointXY(4, 4)) + edge_1 = graph.addEdge(v1, v2, [1]) + edge_2 = graph.addEdge(v2, v1, [1]) + edge_3 = graph.addEdge(v2, v3, [1]) + edge_4 = graph.addEdge(v2, v4, [1]) + edge_5 = graph.addEdge(v3, v4, [1]) + + self.assertEqual(graph.vertexCount(), 4) + self.assertEqual(graph.edgeCount(), 5) + + graph.removeEdge(edge_1) + self.assertEqual(graph.vertexCount(), 4) + self.assertEqual(graph.edgeCount(), 4) + with self.assertRaises(IndexError): + graph.edge(edge_1) + + # make sure vertices are updated accordingly + self.assertEqual(graph.vertex(v1).incomingEdges(), [edge_2]) + self.assertFalse(graph.vertex(v1).outgoingEdges()) + self.assertFalse(graph.vertex(v2).incomingEdges()) + self.assertCountEqual(graph.vertex(v2).outgoingEdges(), [edge_2, edge_3, edge_4]) + + with self.assertRaises(IndexError): + graph.removeEdge(edge_1) + + # remove another edge + graph.removeEdge(edge_2) + self.assertEqual(graph.vertexCount(), 3) + self.assertEqual(graph.edgeCount(), 3) + with self.assertRaises(IndexError): + graph.edge(edge_2) + + # make sure vertices are updated accordingly + # vertex 1 should be removed -- no incoming or outgoing edges remain + with self.assertRaises(IndexError): + graph.vertex(v1) + self.assertFalse(graph.vertex(v2).incomingEdges()) + self.assertCountEqual(graph.vertex(v2).outgoingEdges(), [edge_3, edge_4]) + + with self.assertRaises(IndexError): + graph.removeEdge(edge_2) + + graph.removeEdge(edge_4) + self.assertEqual(graph.vertexCount(), 3) + self.assertEqual(graph.edgeCount(), 2) + with self.assertRaises(IndexError): + graph.edge(edge_4) + self.assertFalse(graph.vertex(v2).incomingEdges()) + self.assertEqual(graph.vertex(v2).outgoingEdges(), [edge_3]) + self.assertEqual(graph.vertex(v4).incomingEdges(), [edge_5]) + self.assertFalse(graph.vertex(v4).outgoingEdges()) + + with self.assertRaises(IndexError): + graph.removeEdge(edge_4) + + graph.removeEdge(edge_3) + self.assertEqual(graph.vertexCount(), 2) + self.assertEqual(graph.edgeCount(), 1) + with self.assertRaises(IndexError): + graph.edge(edge_3) + # v2 should be removed + with self.assertRaises(IndexError): + graph.vertex(v2) + self.assertFalse(graph.vertex(v3).incomingEdges()) + self.assertEqual(graph.vertex(v3).outgoingEdges(), [edge_5]) + + with self.assertRaises(IndexError): + graph.removeEdge(edge_3) + + graph.removeEdge(edge_5) + self.assertEqual(graph.vertexCount(), 0) + self.assertEqual(graph.edgeCount(), 0) + with self.assertRaises(IndexError): + graph.edge(edge_5) + with self.assertRaises(IndexError): + graph.vertex(v3) + with self.assertRaises(IndexError): + graph.vertex(v4) + + with self.assertRaises(IndexError): + graph.removeEdge(edge_5) + if __name__ == '__main__': unittest.main()