diff --git a/CHANGELOG.md b/CHANGELOG.md index 0638b8e60..9734bf115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## 4.11.36 - dev +## 4.11.37 - dev Novas Funcionalidades: @@ -11,6 +11,7 @@ Novas Funcionalidades: - Novo processo de criar grid por meio de informar as coordenadas dos extremos; - Adicionado atalho para ativar/desativar modo reclassificaĆ§Ć£o; - Novo processo de criar pacote de shapefile (utilizado normalmente para preparar a carga no BDGEx); +- Novo processo de generalizar trechos de drenagem de acordo com o comprimento; Melhorias: diff --git a/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/__init__.py b/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/generalizeNetworkEdgesFromLengthAlgorithm.py b/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/generalizeNetworkEdgesFromLengthAlgorithm.py similarity index 60% rename from DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/generalizeNetworkEdgesFromLengthAlgorithm.py rename to DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/generalizeNetworkEdgesFromLengthAlgorithm.py index 96a5f542f..1e862633a 100644 --- a/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/generalizeNetworkEdgesFromLengthAlgorithm.py +++ b/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/generalizeNetworkEdgesFromLengthAlgorithm.py @@ -21,8 +21,8 @@ ***************************************************************************/ """ -from collections import defaultdict -from typing import Dict, Set, Tuple +from itertools import chain +from typing import Dict, List, Optional, Set from PyQt5.QtCore import QCoreApplication from DsgTools.core.GeometricTools import graphHandler from qgis.PyQt.QtCore import QByteArray @@ -33,14 +33,11 @@ QgsProcessingMultiStepFeedback, QgsProcessingParameterEnum, QgsProcessingParameterVectorLayer, - QgsWkbTypes, QgsFeedback, QgsProcessingContext, - QgsGeometry, - QgsFeature, QgsVectorLayer, QgsProcessingParameterNumber, - QgsProcessingParameterBoolean, + QgsProcessingParameterMultipleLayers, ) from ...algRunner import AlgRunner @@ -51,7 +48,9 @@ class GeneralizeNetworkEdgesWithLengthAlgorithm(ValidationAlgorithm): NETWORK_LAYER = "NETWORK_LAYER" MIN_LENGTH = "MIN_LENGTH" GEOGRAPHIC_BOUNDS_LAYER = "GEOGRAPHIC_BOUNDS_LAYER" - WATER_BODIES_LAYER = "WATER_BODIES_LAYER" + POINT_CONSTRAINT_LAYER_LIST = "POINT_CONSTRAINT_LAYER_LIST" + LINE_CONSTRAINT_LAYER_LIST = "LINE_CONSTRAINT_LAYER_LIST" + POLYGON_CONSTRAINT_LAYER_LIST = "POLYGON_CONSTRAINT_LAYER_LIST" METHOD = "METHOD" def initAlgorithm(self, config): @@ -75,10 +74,26 @@ def initAlgorithm(self, config): ) ) self.addParameter( - QgsProcessingParameterVectorLayer( - self.WATER_BODIES_LAYER, - self.tr("Water bodies layer"), - [QgsProcessing.TypeVectorPolygon], + QgsProcessingParameterMultipleLayers( + self.POINT_CONSTRAINT_LAYER_LIST, + self.tr("Point constraint Layers"), + QgsProcessing.TypeVectorPoint, + optional=True, + ) + ) + self.addParameter( + QgsProcessingParameterMultipleLayers( + self.LINE_CONSTRAINT_LAYER_LIST, + self.tr("Line constraint Layers"), + QgsProcessing.TypeVectorLine, + optional=True, + ) + ) + self.addParameter( + QgsProcessingParameterMultipleLayers( + self.POLYGON_CONSTRAINT_LAYER_LIST, + self.tr("Polygon constraint Layers"), + QgsProcessing.TypeVectorPolygon, optional=True, ) ) @@ -125,25 +140,22 @@ def processAlgorithm(self, parameters, context, feedback): # get the network handler self.algRunner = AlgRunner() networkLayer = self.parameterAsLayer(parameters, self.NETWORK_LAYER, context) - filterExpression = self.parameterAsExpression( - parameters, self.FILTER_EXPRESSION, context - ) - if filterExpression == "": - filterExpression = None - oceanLayer = self.parameterAsLayer(parameters, self.WATER_BODIES_LAYER, context) - waterBodyWithFlowLayer = self.parameterAsLayer( - parameters, self.WATER_BODY_WITH_FLOW_LAYER, context - ) - waterSinkLayer = self.parameterAsLayer(parameters, self.SINK_LAYER, context) + threshold = self.parameterAsDouble(parameters, self.MIN_LENGTH, context) geographicBoundsLayer = self.parameterAsLayer( parameters, self.GEOGRAPHIC_BOUNDS_LAYER, context ) - nSteps = ( - 15 - + (runFlowCheck is True) - + (runLoopCheck is True) - + (waterBodyWithFlowLayer is not None) + pointLayerList = self.parameterAsLayerList( + parameters, self.POINT_CONSTRAINT_LAYER_LIST, context + ) + lineLayerList = self.parameterAsLayerList( + parameters, self.LINE_CONSTRAINT_LAYER_LIST, context ) + polygonLayerList = self.parameterAsLayerList( + parameters, self.POLYGON_CONSTRAINT_LAYER_LIST, context + ) + method = self.parameterAsEnum(parameters, self.METHOD, context) + + nSteps = 5 multiStepFeedback = QgsProcessingMultiStepFeedback(nSteps, feedback) currentStep = 0 multiStepFeedback.setCurrentStep(currentStep) @@ -175,7 +187,79 @@ def processAlgorithm(self, parameters, context, feedback): ) currentStep += 1 multiStepFeedback.setCurrentStep(currentStep) + multiStepFeedback.setProgressText(self.tr("Getting constraint points")) + constraintSet = self.getConstraintSet( + nodeDict=nodeDict, + nodesLayer=nodesLayer, + nodeLayerIdDict=nodeLayerIdDict, + geographicBoundsLayer=geographicBoundsLayer, + pointLayerList=pointLayerList, + lineLayerList=lineLayerList, + polygonLayerList=polygonLayerList, + context=context, + feedback=multiStepFeedback, + ) + currentStep += 1 + multiStepFeedback.setCurrentStep(currentStep) + multiStepFeedback.setProgressText(self.tr("Applying algorithm heurisic")) + G_out = graphHandler.generalize_edges_according_to_degrees( + G=networkBidirectionalGraph, + constraintSet=constraintSet, + threshold=threshold, + feedback=multiStepFeedback + ) + idsToRemove = set(networkBidirectionalGraph[a][b]["featid"] for a, b in networkBidirectionalGraph.edges) - set(G_out[a][b]["featid"] for a, b in G_out.edges) + currentStep += 1 + multiStepFeedback.setCurrentStep(currentStep) + if method != 0: + networkLayer.selectByIds(list(idsToRemove), self.selectionIdDict[method]) + return {} + networkLayer.startEditing() + networkLayer.beginEditCommand(self.tr("Deleting features")) + networkLayer.deleteFeatures(list(idsToRemove)) + networkLayer.endEditCommand() return {} + + def getConstraintSet( + self, + nodeDict: Dict[QByteArray, int], + nodesLayer: QgsVectorLayer, + nodeLayerIdDict: Dict[int, Dict[int, QByteArray]], + geographicBoundsLayer: QgsVectorLayer, + pointLayerList = List[QgsVectorLayer], + lineLayerList = List[QgsVectorLayer], + polygonLayerList = List[QgsVectorLayer], + context: Optional[QgsProcessingContext] = None, + feedback: Optional[QgsFeedback] = None, + ) -> Set[int]: + multiStepFeedback = QgsProcessingMultiStepFeedback(4, feedback) + currentStep = 0 + multiStepFeedback.setCurrentStep(currentStep) + (fixedInNodeSet, fixedOutNodeSet) = graphHandler.getInAndOutNodesOnGeographicBounds( + nodeDict=nodeDict, + nodesLayer=nodesLayer, + geographicBoundsLayer=geographicBoundsLayer, + context=context, + feedback=feedback, + ) if geographicBoundsLayer is not None else (set(), set()) + currentStep += 1 + + constraintPointSet = fixedInNodeSet | fixedOutNodeSet + + computeLambda = lambda x: graphHandler.find_constraint_points( + nodesLayer=nodesLayer, + constraintLayer=x, + nodeDict=nodeDict, + nodeLayerIdDict=nodeLayerIdDict, + useBuffer=False, + context=context, + feedback=multiStepFeedback, + ) + + multiStepFeedback.setCurrentStep(currentStep) + constraintPointSetFromLambda = set(i for i in chain.from_iterable(map(computeLambda, pointLayerList + lineLayerList + polygonLayerList))) + constraintPointSet |= constraintPointSetFromLambda + return constraintPointSet def name(self): """ diff --git a/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/reclassifyAdjecentPolygonsAlgorithm.py b/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/reclassifyAdjecentPolygonsAlgorithm.py similarity index 100% rename from DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/reclassifyAdjecentPolygonsAlgorithm.py rename to DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/reclassifyAdjecentPolygonsAlgorithm.py diff --git a/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/splitPolygonsAlgorithm.py b/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/splitPolygonsAlgorithm.py similarity index 100% rename from DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/splitPolygonsAlgorithm.py rename to DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/splitPolygonsAlgorithm.py diff --git a/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/splitPolygonsByGrid.py b/DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/splitPolygonsByGrid.py similarity index 100% rename from DsgTools/core/DSGToolsProcessingAlgs/Algs/GeometricAlgs/splitPolygonsByGrid.py rename to DsgTools/core/DSGToolsProcessingAlgs/Algs/GeneralizationAlgs/splitPolygonsByGrid.py diff --git a/DsgTools/core/DSGToolsProcessingAlgs/Algs/ValidationAlgs/fixNetworkAlgorithm.py b/DsgTools/core/DSGToolsProcessingAlgs/Algs/ValidationAlgs/fixNetworkAlgorithm.py index a2e440774..aa678a00e 100644 --- a/DsgTools/core/DSGToolsProcessingAlgs/Algs/ValidationAlgs/fixNetworkAlgorithm.py +++ b/DsgTools/core/DSGToolsProcessingAlgs/Algs/ValidationAlgs/fixNetworkAlgorithm.py @@ -22,28 +22,15 @@ """ from PyQt5.QtCore import QCoreApplication -import processing from DsgTools.core.GeometricTools.layerHandler import LayerHandler from qgis.core import ( - QgsDataSourceUri, - QgsFeature, - QgsFeatureSink, - QgsGeometry, QgsProcessing, - QgsProcessingAlgorithm, QgsProcessingMultiStepFeedback, QgsProcessingOutputVectorLayer, QgsProcessingParameterBoolean, QgsProcessingParameterDistance, - QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterFeatureSource, QgsProcessingParameterField, - QgsProcessingParameterMultipleLayers, - QgsProcessingParameterNumber, QgsProcessingParameterVectorLayer, - QgsProcessingUtils, - QgsSpatialIndex, QgsWkbTypes, ) diff --git a/DsgTools/core/DSGToolsProcessingAlgs/dsgtoolsProcessingAlgorithmProvider.py b/DsgTools/core/DSGToolsProcessingAlgs/dsgtoolsProcessingAlgorithmProvider.py index 153c9199d..12965fe74 100644 --- a/DsgTools/core/DSGToolsProcessingAlgs/dsgtoolsProcessingAlgorithmProvider.py +++ b/DsgTools/core/DSGToolsProcessingAlgs/dsgtoolsProcessingAlgorithmProvider.py @@ -33,6 +33,7 @@ from DsgTools.core.DSGToolsProcessingAlgs.Algs.DataManagementAlgs.appendFeaturesToLayerAlgorithm import ( AppendFeaturesToLayerAlgorithm, ) +from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeneralizationAlgs.generalizeNetworkEdgesFromLengthAlgorithm import GeneralizeNetworkEdgesWithLengthAlgorithm from DsgTools.core.DSGToolsProcessingAlgs.Algs.LayerManagementAlgs.buildZipPackagesAlgorithm import BuildZipPackageAlgorithm from DsgTools.core.DSGToolsProcessingAlgs.Algs.OtherAlgs.createGridFromCoordinatesAlgorithm import CreateGridFromCoordinatesAlgorithm from DsgTools.core.DSGToolsProcessingAlgs.Algs.ValidationAlgs.fixSegmentErrorsBetweenLinesAlgorithm import ( @@ -67,7 +68,7 @@ from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeometricAlgs.line2Multiline import ( Line2Multiline, ) -from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeometricAlgs.reclassifyAdjecentPolygonsAlgorithm import ( +from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeneralizationAlgs.reclassifyAdjecentPolygonsAlgorithm import ( ReclassifyAdjacentPolygonsAlgorithm, ) from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeometricAlgs.selectByDE9IM import ( @@ -76,10 +77,10 @@ from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeometricAlgs.smallHoleRemoverAlgorithm import ( SmallHoleRemoverAlgorithm, ) -from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeometricAlgs.splitPolygonsAlgorithm import ( +from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeneralizationAlgs.splitPolygonsAlgorithm import ( SplitPolygons, ) -from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeometricAlgs.splitPolygonsByGrid import ( +from DsgTools.core.DSGToolsProcessingAlgs.Algs.GeneralizationAlgs.splitPolygonsByGrid import ( SplitPolygonsByGrid, ) from DsgTools.core.DSGToolsProcessingAlgs.Algs.LayerManagementAlgs.applyStylesFromDatabaseToLayersAlgorithm import ( @@ -624,6 +625,7 @@ def getAlgList(self): IdentifyWaterBodyAndContourInconsistencies(), CreateGridFromCoordinatesAlgorithm(), BuildZipPackageAlgorithm(), + GeneralizeNetworkEdgesWithLengthAlgorithm(), ] return algList diff --git a/DsgTools/core/GeometricTools/graphHandler.py b/DsgTools/core/GeometricTools/graphHandler.py index 1498c3bfc..05c4ee0f7 100644 --- a/DsgTools/core/GeometricTools/graphHandler.py +++ b/DsgTools/core/GeometricTools/graphHandler.py @@ -19,19 +19,12 @@ * * ***************************************************************************/ """ -import itertools -import operator -import concurrent.futures -from functools import reduce -from collections import defaultdict, Counter +from collections import defaultdict from itertools import tee -import os from typing import Any, Dict, Iterable, List, Optional, Set, Tuple from qgis.PyQt.QtCore import QByteArray from itertools import chain from itertools import product -from itertools import starmap -from functools import partial from qgis.core import ( QgsGeometry, @@ -40,6 +33,7 @@ QgsVectorLayer, QgsFeedback, QgsProcessingContext, + QgsWkbTypes, ) from DsgTools.core.DSGToolsProcessingAlgs.algRunner import AlgRunner @@ -963,3 +957,162 @@ def buildAuxLayersPriorGraphBuilding( feedback=multiStepFeedback, ) return localCache, nodesLayer + +def getInAndOutNodesOnGeographicBounds( + nodeDict: Dict[QByteArray, int], + nodesLayer: QgsVectorLayer, + geographicBoundsLayer: QgsVectorLayer, + context: Optional[QgsProcessingContext] = None, + feedback: Optional[QgsFeedback] = None, + ) -> Tuple[Set[int], Set[int]]: + """ + Get the in-nodes and out-nodes that fall within the geographic bounds. + + Args: + self: The instance of the class. + nodeDict: A dictionary mapping node geometry to an auxiliary ID. + nodesLayer: A QgsVectorLayer representing nodes in the network. + geographicBoundsLayer: The geographic bounds layer. + context: The context object for the processing. + feedback: The QgsFeedback object for providing feedback during processing. + + Returns: + A tuple containing two sets: fixedInNodeSet and fixedOutNodeSet. + - fixedInNodeSet: A set of in-nodes that fall within the geographic bounds. + - fixedOutNodeSet: A set of out-nodes that fall within the geographic bounds. + + Notes: + This function performs the following steps: + 1. Creates a spatial index for the nodesLayer. + 2. Extracts the nodes that are outside the geographic bounds. + 3. Iterates over the nodes outside the geographic bounds and adds them to the appropriate set. + 4. Returns the sets of in-nodes and out-nodes within the geographic bounds. + + The feedback object is used to monitor the progress of the function. + """ + multiStepFeedback = QgsProcessingMultiStepFeedback(3, feedback) if feedback is not None else None + context = context if context is not None else QgsProcessingContext() + algRunner = AlgRunner() + currentStep = 0 + if multiStepFeedback is not None: + multiStepFeedback.setCurrentStep(currentStep) + algRunner.runCreateSpatialIndex( + inputLyr=nodesLayer, + context=context, + feedback=multiStepFeedback, + is_child_algorithm=True, + ) + currentStep += 1 + if multiStepFeedback is not None: + multiStepFeedback.setCurrentStep(currentStep) + nodesOutsideGeographicBounds = algRunner.runExtractByLocation( + inputLyr=nodesLayer, + intersectLyr=geographicBoundsLayer, + predicate=[AlgRunner.Disjoint], + context=context, + feedback=multiStepFeedback, + ) + currentStep += 1 + if multiStepFeedback is not None: + multiStepFeedback.setCurrentStep(currentStep) + fixedInNodeSet, fixedOutNodeSet = set(), set() + nFeats = nodesOutsideGeographicBounds.featureCount() + if nFeats == 0: + return fixedInNodeSet, fixedOutNodeSet + stepSize = 100 / nFeats + for current, nodeFeat in enumerate(nodesOutsideGeographicBounds.getFeatures()): + if multiStepFeedback is not None and multiStepFeedback.isCanceled(): + break + selectedSet = ( + fixedInNodeSet if nodeFeat["vertex_pos"] == 0 else fixedOutNodeSet + ) + geom = nodeFeat.geometry() + selectedSet.add(nodeDict[geom.asWkb()]) + if multiStepFeedback is not None: + multiStepFeedback.setProgress(current * stepSize) + return fixedInNodeSet, fixedOutNodeSet + +def find_constraint_points( + nodesLayer: QgsVectorLayer, + constraintLayer: QgsVectorLayer, + nodeDict: Dict[QByteArray, int], + nodeLayerIdDict: Dict[int, Dict[int, QByteArray]], + useBuffer: bool =True, + context: Optional[QgsProcessingContext] = None, + feedback: Optional[QgsFeedback] = None, +) -> Set[int]: + multiStepFeedback = QgsProcessingMultiStepFeedback(3, feedback) if feedback is not None else None + context = context if context is not None else QgsProcessingContext() + algRunner = AlgRunner() + constraintSet = set() + layerToRelate = algRunner.runBuffer( + inputLayer=constraintLayer, + distance=1e-6, + context=context, + is_child_algorithm=True, + ) if constraintLayer.geometryType() != QgsWkbTypes.PointGeometry and useBuffer else constraintLayer + predicate = AlgRunner.Intersect if constraintLayer.geometryType() != QgsWkbTypes.PointGeometry else AlgRunner.Equal + selectedNodesFromOcean = algRunner.runExtractByLocation( + inputLyr=nodesLayer, + intersectLyr=layerToRelate, + context=context, + predicate=[predicate], + feedback=multiStepFeedback, + ) + for feat in selectedNodesFromOcean.getFeatures(): + if multiStepFeedback.isCanceled(): + break + constraintSet.add(nodeDict[nodeLayerIdDict[feat["nfeatid"]]]) + return constraintSet + +def generalize_edges_according_to_degrees( + G, + constraintSet: Set[int], + threshold: float, + feedback: Optional[QgsFeedback] = None, +): + G_copy = G.copy() + pairsToRemove = find_smaller_first_order_path_with_length_smaller_than_threshold( + G=G_copy, + constraintSet=constraintSet, + threshold=threshold, + feedback=feedback + ) + while pairsToRemove is not None: + if feedback is not None and feedback.isCanceled(): + break + for n0, n1 in pairsToRemove: + G_copy.remove_edge(n0, n1) + pairsToRemove = find_smaller_first_order_path_with_length_smaller_than_threshold( + G=G_copy, + constraintSet=constraintSet, + threshold=threshold, + feedback=feedback + ) + return G_copy + + +def find_smaller_first_order_path_with_length_smaller_than_threshold( + G, + constraintSet: Set[int], + threshold: float, + feedback: Optional[QgsFeedback] = None +) -> frozenset[frozenset]: + total_length_dict = dict() + edges_to_remove_dict = dict() + for node in set(node for node in G.nodes if G.degree(node) == 1 and node not in constraintSet): + if feedback is not None and feedback.isCanceled(): + return None + connectedNodes = fetch_connected_nodes(G, node, 2) + if set(connectedNodes).intersection(constraintSet): + continue + pairs = frozenset([frozenset([a, b]) for i in connectedNodes for a, b in G.edges(i)]) + total_length = sum(G[a][b]["length"] for a, b in pairs) + if total_length >= threshold: + continue + edges_to_remove_dict[node] = pairs + total_length_dict[node] = total_length + if len(total_length_dict) == 0: + return None + smaller_path_node = min(total_length_dict.keys(), key=lambda x: total_length_dict[x]) + return edges_to_remove_dict[smaller_path_node] diff --git a/DsgTools/metadata.txt b/DsgTools/metadata.txt index b280c4bb1..bc3e64121 100644 --- a/DsgTools/metadata.txt +++ b/DsgTools/metadata.txt @@ -10,7 +10,7 @@ name=DSG Tools qgisMinimumVersion=3.22 description=Brazilian Army Cartographic Production Tools -version=4.11.36 +version=4.11.37 author=Brazilian Army Geographic Service email=dsgtools@dsg.eb.mil.br about=