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
This commit is contained in:
Nyall Dawson 2021-11-08 10:41:56 +10:00
parent 9b02c301a5
commit c0b253a69b
4 changed files with 357 additions and 0 deletions

View File

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

View File

@ -19,6 +19,7 @@
*/
#include "qgsgraph.h"
#include <QSet>
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();

View File

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

View File

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