31using namespace Qt::StringLiterals;
35QString QgsValidateNetworkAlgorithm::name()
const
37 return u
"validatenetwork"_s;
40QString QgsValidateNetworkAlgorithm::displayName()
const
42 return QObject::tr(
"Validate network" );
45QStringList QgsValidateNetworkAlgorithm::tags()
const
47 return QObject::tr(
"topological,topology,check,graph,shortest,path" ).split(
',' );
50QString QgsValidateNetworkAlgorithm::group()
const
52 return QObject::tr(
"Network analysis" );
55QString QgsValidateNetworkAlgorithm::groupId()
const
57 return u
"networkanalysis"_s;
60QIcon QgsValidateNetworkAlgorithm::icon()
const
65QString QgsValidateNetworkAlgorithm::svgIconPath()
const
70QString QgsValidateNetworkAlgorithm::shortDescription()
const
72 return QObject::tr(
"Validates a network line layer, identifying data and topology errors that may affect network analysis tools." );
75QString QgsValidateNetworkAlgorithm::shortHelpString()
const
78 "This algorithm analyzes a network vector layer to identify data and topology errors "
79 "that may affect network analysis tools (like shortest path).\n\n"
80 "Optional checks include:\n\n"
81 "1. Validating the 'Direction' field to ensure all direction field values in the input layer "
82 "match the configured forward/backward/both values. Errors will be reported if the direction field "
83 "value is non-null and does not match one of the configured values.\n"
84 "2. Checking node-to-node separation. This check identifies nodes from the network graph that "
85 "are closer to other nodes than the specified tolerance distance. This often indicates missed "
86 "snaps or short segments in the input layer. In the case that a node violates this condition with multiple other "
87 "nodes, only the closest violation will be reported.\n"
88 "3. Checking node-to-segment separation: This check identifies nodes that are closer to a line "
89 "segment (e.g. a graph edge) than the specified tolerance distance, without being connected to it. In the case "
90 "that a node violates this condition with multiple other edges, only the closest violation will be reported.\n\n"
91 "Topology checks (node-to-node and node-to-segment) can optionally be restricted to only evaluate nodes that are topological dead-ends (connected to only one other distinct node). This is useful "
92 "for specifically targeting dangles or undershoots.\n\n"
93 "Two layers are output by this algorithm:\n"
94 "1. An output containing features from the original network layer which failed the direction validation checks.\n"
95 "2. An output representing the problematic node locations with a 'error' field explaining the error. This is "
96 "a line layer, where the output features join the problematic node to the node or "
97 "segment which failed the tolerance checks."
101QgsValidateNetworkAlgorithm *QgsValidateNetworkAlgorithm::createInstance()
const
103 return new QgsValidateNetworkAlgorithm();
106void QgsValidateNetworkAlgorithm::initAlgorithm(
const QVariantMap & )
110 auto separationNodeNodeParam = std::make_unique<QgsProcessingParameterDistance>( u
"TOLERANCE_NODE_NODE"_s, QObject::tr(
"Minimum separation between nodes" ), QVariant(), u
"INPUT"_s,
true );
112 separationNodeNodeParam->setHelp(
114 "The minimum allowed distance between two distinct graph nodes.\n\n"
115 "Nodes closer than this distance (but not identical) will be flagged as errors.\n\n"
116 "Leave empty to disable this check."
119 addParameter( separationNodeNodeParam.release() );
121 auto separationNodeSegmentParam
122 = std::make_unique<QgsProcessingParameterDistance>( u
"TOLERANCE_NODE_SEGMENT"_s, QObject::tr(
"Minimum separation between nodes and non-noded segments" ), QVariant(), u
"INPUT"_s,
true );
124 separationNodeSegmentParam->setHelp(
126 "The minimum allowed distance between a graph node and a graph edge (segment) "
127 "that is not connected to the node.\n\n"
128 "Nodes closer to a segment than this distance "
129 "will be flagged. Leave empty to disable this check."
132 addParameter( separationNodeSegmentParam.release() );
134 auto endpointsOnlyParam = std::make_unique<QgsProcessingParameterBoolean>( u
"ENDPOINTS_ONLY"_s, QObject::tr(
"Only check for errors at end points" ),
false );
135 endpointsOnlyParam->setHelp(
136 QObject::tr(
"If checked, topology checks (node-to-node and node-to-segment) will only be evaluated for nodes that are topological dead-ends (connected to only one other distinct node)." )
138 addParameter( endpointsOnlyParam.release() );
142 directionField->setHelp( QObject::tr(
"The attribute field specifying the direction of traffic flow for each segment." ) );
143 addParameter( directionField.release() );
145 auto forwardValue = std::make_unique<QgsProcessingParameterString>( u
"VALUE_FORWARD"_s, QObject::tr(
"Value for forward direction" ), QVariant(),
false,
true );
146 forwardValue->setHelp( QObject::tr(
"The string value in the direction field that indicates one-way traffic in the digitized direction." ) );
147 addParameter( forwardValue.release() );
149 auto backwardValue = std::make_unique<QgsProcessingParameterString>( u
"VALUE_BACKWARD"_s, QObject::tr(
"Value for backward direction" ), QVariant(),
false,
true );
150 backwardValue->setHelp( QObject::tr(
"The string value in the direction field that indicates one-way traffic opposite to the digitized direction." ) );
151 addParameter( backwardValue.release() );
153 auto bothValue = std::make_unique<QgsProcessingParameterString>( u
"VALUE_BOTH"_s, QObject::tr(
"Value for both directions" ), QVariant(),
false,
true );
154 bothValue->setHelp( QObject::tr(
"The string value in the direction field that indicates two-way traffic." ) );
155 addParameter( bothValue.release() );
157 std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterDistance>( u
"TOLERANCE"_s, QObject::tr(
"Topology tolerance" ), 0, u
"INPUT"_s,
false, 0 );
159 addParameter( tolerance.release() );
161 auto invalidNetworkOutput
162 = std::make_unique< QgsProcessingParameterFeatureSink >( u
"OUTPUT_INVALID_NETWORK"_s, QObject::tr(
"Invalid network features" ),
Qgis::ProcessingSourceType::VectorLine, QVariant(),
true,
true );
163 invalidNetworkOutput->setHelp(
165 "Output line layer containing geometries representing features from the network layer with validity errors.\n\n"
166 "This output includes an attribute explaining why each feature is invalid."
169 addParameter( invalidNetworkOutput.release() );
171 addOutput(
new QgsProcessingOutputNumber( u
"COUNT_INVALID_NETWORK_FEATURES"_s, QObject::tr(
"Count of invalid network features" ) ) );
173 auto invalidNodeOutput
174 = std::make_unique< QgsProcessingParameterFeatureSink >( u
"OUTPUT_INVALID_NODES"_s, QObject::tr(
"Invalid network nodes" ),
Qgis::ProcessingSourceType::VectorLine, QVariant(),
true,
true );
175 invalidNodeOutput->setHelp(
177 "Output line layer containing geometries representing nodes from the network layer with validity errors.\n\n"
178 "This output includes an attribute explaining why each node is invalid."
181 addParameter( invalidNodeOutput.release() );
188 QGS_MARK_ALGORITHM_SOURCE
190 std::unique_ptr<QgsFeatureSource> networkSource( parameterAsSource( parameters, u
"INPUT"_s, context ) );
191 if ( !networkSource )
194 const QString directionFieldName = parameterAsString( parameters, u
"DIRECTION_FIELD"_s, context );
195 const QString forwardValue = parameterAsString( parameters, u
"VALUE_FORWARD"_s, context );
196 const QString backwardValue = parameterAsString( parameters, u
"VALUE_BACKWARD"_s, context );
197 const QString bothValue = parameterAsString( parameters, u
"VALUE_BOTH"_s, context );
198 const double tolerance = parameterAsDouble( parameters, u
"TOLERANCE"_s, context );
200 const bool checkEndpointsOnly = parameterAsBoolean( parameters, u
"ENDPOINTS_ONLY"_s, context );
202 double toleranceNodeToNode = 0;
203 bool checkNodeToNodeDistance =
false;
204 if ( parameters.value( u
"TOLERANCE_NODE_NODE"_s ).isValid() )
206 toleranceNodeToNode = parameterAsDouble( parameters, u
"TOLERANCE_NODE_NODE"_s, context );
207 checkNodeToNodeDistance = ( toleranceNodeToNode > 0 );
210 double toleranceNodeToSegment = 0;
211 bool checkNodeToSegmentDistance =
false;
212 if ( parameters.value( u
"TOLERANCE_NODE_SEGMENT"_s ).isValid() )
214 toleranceNodeToSegment = parameterAsDouble( parameters, u
"TOLERANCE_NODE_SEGMENT"_s, context );
215 checkNodeToSegmentDistance = ( toleranceNodeToSegment > 0 );
219 newNetworkErrorFields.
append(
QgsField( u
"error"_s, QMetaType::Type::QString ) );
222 QString networkErrorDest;
223 std::unique_ptr<QgsFeatureSink> networkErrorSink(
224 parameterAsSink( parameters, u
"OUTPUT_INVALID_NETWORK"_s, context, networkErrorDest, networkErrorFields, networkSource->wkbType(), networkSource->sourceCrs() )
228 nodeErrorFields.
append(
QgsField( u
"error"_s, QMetaType::Type::QString ) );
230 QString nodeErrorDest;
231 std::unique_ptr<QgsFeatureSink> nodeErrorSink( parameterAsSink( parameters, u
"OUTPUT_INVALID_NODES"_s, context, nodeErrorDest, nodeErrorFields,
Qgis::WkbType::LineString, networkSource->sourceCrs() ) );
234 multiFeedback.setStepWeights( { 10, 40, 10, 40 } );
235 multiFeedback.setCurrentStep( 0 );
238 if ( networkErrorSink )
239 outputs.insert( u
"OUTPUT_INVALID_NETWORK"_s, networkErrorDest );
241 outputs.insert( u
"OUTPUT_INVALID_NODES"_s, nodeErrorDest );
244 int directionFieldIdx = -1;
245 long long countInvalidFeatures = 0;
246 if ( !directionFieldName.isEmpty() )
248 directionFieldIdx = networkSource->fields().lookupField( directionFieldName );
249 if ( directionFieldIdx < 0 )
251 throw QgsProcessingException( QObject::tr(
"Missing field %1 in input layer" ).arg( directionFieldName ) );
254 multiFeedback.pushInfo( QObject::tr(
"Validating direction attributes…" ) );
255 const long long count = networkSource->featureCount();
256 long long current = 0;
257 const double step = count > 0 ? 100.0 /
static_cast< double >( count ) : 1;
263 if ( multiFeedback.isCanceled() )
266 const QVariant val = feature.
attribute( directionFieldIdx );
269 const QString directionValueString = val.toString();
270 if ( directionValueString != forwardValue && directionValueString != backwardValue && directionValueString != bothValue )
272 if ( networkErrorSink )
276 outputFeatureAttrs.append( QObject::tr(
"Invalid direction value: '%1'" ).arg( directionValueString ) );
280 throw QgsProcessingException( writeFeatureError( networkErrorSink.get(), parameters, u
"OUTPUT_INVALID_NETWORK"_s ) );
287 countInvalidFeatures++;
292 multiFeedback.setProgress(
static_cast< double >( current ) * step );
295 if ( networkErrorSink )
297 networkErrorSink->finalize();
302 outputs.insert( u
"COUNT_INVALID_NETWORK_FEATURES"_s, countInvalidFeatures );
303 if ( countInvalidFeatures > 0 )
305 multiFeedback.reportError( QObject::tr(
"Found %1 invalid network features" ).arg( countInvalidFeatures ) );
308 if ( !checkNodeToNodeDistance && !checkNodeToSegmentDistance )
314 multiFeedback.pushInfo( QObject::tr(
"Building graph for topology validation…" ) );
315 multiFeedback.setCurrentStep( 1 );
320 QVector<QgsPointXY> snappedPoints;
321 director.makeGraph( &builder, {}, snappedPoints, &multiFeedback );
323 std::unique_ptr<QgsGraph> graph( builder.takeGraph() );
325 if ( multiFeedback.isCanceled() )
328 multiFeedback.pushInfo( QObject::tr(
"Indexing graph nodes and edges…" ) );
329 multiFeedback.setCurrentStep( 2 );
337 const int vertexCount = graph->vertexCount();
339 const long long totalGraphElements = ( checkNodeToNodeDistance ? vertexCount : 0 ) + ( checkNodeToSegmentDistance ? graph->edgeCount() : 0 );
340 const double indexStep = totalGraphElements > 0 ? 100.0 /
static_cast< double >( totalGraphElements ) : 1;
341 long long elementsProcessed = 0;
343 if ( checkNodeToNodeDistance )
345 for (
int i = 0; i < vertexCount; ++i )
347 if ( multiFeedback.isCanceled() )
349 nodeIndex.
addFeature( i, graph->vertex( i ).point() );
351 multiFeedback.setProgress(
static_cast< double >( elementsProcessed ) * indexStep );
356 if ( checkNodeToSegmentDistance )
358 for (
int i = 0; i < graph->edgeCount(); ++i )
360 if ( multiFeedback.isCanceled() )
369 multiFeedback.setProgress(
static_cast< double >( elementsProcessed ) * indexStep );
374 multiFeedback.pushInfo( QObject::tr(
"Validating graph topology…" ) );
375 multiFeedback.setCurrentStep( 2 );
377 const double topoStep = vertexCount > 0 ? 100.0 / vertexCount : 1;
383 double distance = std::numeric_limits<double>::max();
386 QSet< QPair< long long, long long > > alreadyReportedNodes;
387 long long countInvalidNodes = 0;
389 for (
long long i = 0; i < vertexCount; ++i )
391 if ( multiFeedback.isCanceled() )
398 bool evaluateNode =
true;
400 if ( checkEndpointsOnly )
403 QSet<int> adjacentNodeIndices;
406 adjacentNodeIndices.insert( graph->edge( edgeId ).toVertex() );
410 adjacentNodeIndices.insert( graph->edge( edgeId ).fromVertex() );
412 if ( adjacentNodeIndices.count() != 1 )
414 evaluateNode =
false;
418 if ( evaluateNode && checkNodeToNodeDistance )
420 const std::vector< QgsVectorLayerDirector::VertexSourceInfo > &fidsFirstNode = director.sourcesForVertex( i );
425 NodeError closestError;
436 if ( graph->edge( edge ).fromVertex() == i )
446 if ( graph->edge( edge ).toVertex() == data.id )
455 const std::vector<QgsVectorLayerDirector::VertexSourceInfo> &fidsSecondNode = director.sourcesForVertex( data.id );
457 bool shareCommonFeature =
false;
462 if ( info1 == info2 )
464 shareCommonFeature =
true;
468 if ( shareCommonFeature )
472 if ( shareCommonFeature )
478 const double distanceNodeToNode = pt.
distance( data.point() );
479 if ( distanceNodeToNode < toleranceNodeToNode && distanceNodeToNode < closestError.distance )
481 closestError.distance = distanceNodeToNode;
482 closestError.id = data.id;
483 closestError.pt = data.point();
487 if ( !closestError.pt.isEmpty() )
489 const QPair< long long, long long > nodeId = qMakePair( std::min( closestError.id, i ), std::max( closestError.id, i ) );
490 if ( alreadyReportedNodes.contains( nodeId ) )
495 alreadyReportedNodes.insert( nodeId );
499 QgsFeature nodeErrorFeature( nodeErrorFields );
500 nodeErrorFeature.setGeometry( std::make_unique< QgsLineString >( QVector<QgsPointXY>() << pt << closestError.pt ) );
501 nodeErrorFeature.setAttributes(
QgsAttributes() << QObject::tr(
"Node too close to adjacent node (%1 < %2)" ).arg( closestError.distance ).arg( toleranceNodeToNode ) );
503 throw QgsProcessingException( writeFeatureError( nodeErrorSink.get(), parameters, u
"OUTPUT_INVALID_NODES"_s ) );
511 if ( evaluateNode && checkNodeToSegmentDistance )
514 NodeError closestError;
516 const QList<QgsFeatureId> edgeIds = edgeIndex.intersects(
QgsRectangle::fromCenterAndSize( pt, toleranceNodeToSegment * 2, toleranceNodeToSegment * 2 ) );
519 const QgsGraphEdge &edge = graph->edge(
static_cast< int >( edgeIdx ) );
528 const double distanceToSegment = std::sqrt( pt.
sqrDistToSegment( p1.
x(), p1.
y(), p2.
x(), p2.
y(), closestPt ) );
529 if ( distanceToSegment >= toleranceNodeToSegment )
539 if ( distanceToSegment > closestError.distance )
543 closestError.distance = distanceToSegment;
544 closestError.pt = closestPt;
547 if ( !closestError.pt.isEmpty() )
551 QgsFeature nodeErrorFeature( nodeErrorFields );
552 nodeErrorFeature.setGeometry( std::make_unique< QgsLineString >( QVector<QgsPointXY>() << pt << closestError.pt ) );
553 nodeErrorFeature.setAttributes(
QgsAttributes() << QObject::tr(
"Node too close to non-noded segment (%1 < %2)" ).arg( closestError.distance ).arg( toleranceNodeToSegment ) );
555 throw QgsProcessingException( writeFeatureError( nodeErrorSink.get(), parameters, u
"OUTPUT_INVALID_NODES"_s ) );
563 multiFeedback.setProgress(
static_cast< double >( i ) * topoStep );
568 nodeErrorSink->finalize();
573 if ( countInvalidNodes > 0 )
575 multiFeedback.reportError( QObject::tr(
"Found %1 invalid network nodes" ).arg( countInvalidNodes ) );
578 outputs.insert( u
"COUNT_INVALID_NODES"_s, countInvalidNodes );
@ VectorLine
Vector line layers.
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
@ Optional
Parameter is optional.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QString iconPath(const QString &iconFile)
Returns path to the desired icon file.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Q_INVOKABLE QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
void setProgress(double progress)
Sets the current progress for the feedback object.
Encapsulate a field in an attribute table or data source.
Container of fields for a vector layer.
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
Used for making the QgsGraph object.
Represents an edge in a graph.
int fromVertex() const
Returns the index of the vertex at the start of this edge.
int toVertex() const
Returns the index of the vertex at the end of this edge.
Represents vertex in a graph.
QgsGraphEdgeIds outgoingEdges() const
Returns outgoing edge ids, i.e.
QgsGraphEdgeIds incomingEdges() const
Returns the incoming edge ids, i.e.
QgsPointXY point() const
Returns point associated with graph vertex.
double distance(double x, double y) const
Returns the distance between this point and a specified x, y coordinate.
bool compare(const QgsPointXY &other, double epsilon=4 *std::numeric_limits< double >::epsilon()) const
Compares this point with another point with a fuzzy tolerance.
double sqrDistToSegment(double x1, double y1, double x2, double y2, QgsPointXY &minDistPoint, double epsilon=Qgis::DEFAULT_SEGMENT_EPSILON) const
Returns the minimum distance between this point and a segment.
Contains information about the context in which a processing algorithm is executed.
QString ellipsoid() const
Returns the ellipsoid to use for distance and area calculations.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
void featureAddedToSink(const QString &output)
Reports that a feature was added to the the sink associated with the specified algorithm output.
void featureSinkFinalized(const QString &output)
Reports that a feature sink has been finalized.
Processing feedback object for multi-step operations.
A numeric output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
static QgsFields combineFields(const QgsFields &fieldsA, const QgsFields &fieldsB, const QString &fieldsBPrefix=QString())
Combines two field lists, avoiding duplicate field names (in a case-insensitive manner).
A rectangle specified with double values.
static QgsRectangle fromCenterAndSize(const QgsPointXY ¢er, double width, double height)
Creates a new rectangle, given the specified center point and width and height.
A container for data stored inside a QgsSpatialIndexKDBush index.
A very fast static spatial index for 2D points based on a flat KD-tree.
void finalize()
Finalizes the index after manually adding features.
QList< QgsSpatialIndexKDBushData > intersects(const QgsRectangle &rectangle) const
Returns the list of features which fall within the specified rectangle.
bool addFeature(QgsFeatureId id, const QgsPointXY &point)
Adds a single feature to the index.
A spatial index for QgsFeature objects.
@ FlagStoreFeatureGeometries
Indicates that the spatial index should also store feature geometries. This requires more memory,...
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
Determines creating a graph from a vector line layer.
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Represents information about a graph node's source vertex.