def testGetGeometry(self): idx = QgsSpatialIndex() idx2 = QgsSpatialIndex(QgsSpatialIndex.FlagStoreFeatureGeometries) fid = 0 for y in range(5): for x in range(10, 15): ft = QgsFeature() ft.setId(fid) ft.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y))) idx.insertFeature(ft) idx2.insertFeature(ft) fid += 1 # not storing geometries, a keyerror should be raised with self.assertRaises(KeyError): idx.geometry(-100) with self.assertRaises(KeyError): idx.geometry(1) with self.assertRaises(KeyError): idx.geometry(2) with self.assertRaises(KeyError): idx.geometry(1000) self.assertEqual(idx2.geometry(1).asWkt(1), 'Point (11 0)') self.assertEqual(idx2.geometry(2).asWkt(1), 'Point (12 0)') with self.assertRaises(KeyError): idx2.geometry(-100) with self.assertRaises(KeyError): idx2.geometry(1000)
class GsCollection: """Class used for managing the QgsFeature spatially. QgsSpatialIndex class is used to store and retrieve the features. """ __slots__ = ('_spatial_index', '_dict_qgs_segment', '_id_qgs_segment') def __init__(self): """Constructor that initialize the GsCollection. """ self._spatial_index = QgsSpatialIndex() self._dict_qgs_segment = { } # Contains a reference to the original geometry self._id_qgs_segment = 0 def _get_next_id_segment(self): """Increment the id of the segment. :return: Value of the next ID :rtype: int """ self._id_qgs_segment += 1 return self._id_qgs_segment def _create_rectangle(self, geom_id, qgs_geom): """Creates a new QgsRectangle to load in the QgsSpatialIndex. :param: geom_id: Integer ID of the geometry :param: qgs_geom: QgsGeometry to use for bounding box extraction :return: The feature created :rtype: QgsFeature """ id_segment = self._get_next_id_segment() self._dict_qgs_segment[id_segment] = ( geom_id, qgs_geom) # Reference to the RbGeom ID and geometry return id_segment, qgs_geom.boundingBox() def add_features(self, rb_geoms, feedback): """Add a RbGeom object in the spatial index. For the LineString geometries. The geometry is broken into each line segment that are individually loaded in the QgsSpatialIndex. This strategy accelerate the validation of the spatial constraints. :param: rb_geoms: List of RbGeom to load in the QgsSpatialIndex :feedback: QgsFeedback handle used to update the progress bar """ progress_bar = ProgressBar(feedback, len(rb_geoms), "Building internal structure...") for val, rb_geom in enumerate(rb_geoms): progress_bar.set_value(val) qgs_rectangles = [] if rb_geom.qgs_geom.wkbType() == QgsWkbTypes.Point: qgs_rectangles.append( self._create_rectangle(rb_geom.id, rb_geom.qgs_geom)) else: qgs_points = rb_geom.qgs_geom.constGet().points() for i in range(0, (len(qgs_points) - 1)): qgs_geom = QgsGeometry( QgsLineString(qgs_points[i], qgs_points[i + 1])) qgs_rectangles.append( self._create_rectangle(rb_geom.id, qgs_geom)) for geom_id, qgs_rectangle in qgs_rectangles: self._spatial_index.addFeature(geom_id, qgs_rectangle) return def get_segment_intersect(self, qgs_geom_id, qgs_rectangle, qgs_geom_subline): """Find the feature that intersects the bounding box. Once the line string intersecting the bounding box are found. They are separated into 2 lists. The first one being the line string with the same id (same line) the second one all the others line string. :param qgs_geom_id: ID of the line string that is being simplified :param qgs_rectangle: QgsRectangle used for feature intersection :param qgs_geom_subline: LineString used to remove line segment superimposed to this line string :return: Two lists of line string segment. First: Line string with same id; Second all the others :rtype: tuple of 2 lists """ qgs_geoms_with_itself = [] qgs_geoms_with_others = [] qgs_rectangle.grow( Epsilon.ZERO_RELATIVE * 100.) # Always increase the b_box to avoid degenerated b_box ids = self._spatial_index.intersects(qgs_rectangle) for geom_id in ids: target_qgs_geom_id, target_qgs_geom = self._dict_qgs_segment[ geom_id] if target_qgs_geom_id is None: # Nothing to do; segment was deleted pass else: if target_qgs_geom_id == qgs_geom_id: # Test that the segment is not part of qgs_subline if not target_qgs_geom.within(qgs_geom_subline): qgs_geoms_with_itself.append(target_qgs_geom) else: qgs_geoms_with_others.append(target_qgs_geom) return qgs_geoms_with_itself, qgs_geoms_with_others def _delete_segment(self, qgs_geom_id, qgs_pnt0, qgs_pnt1): """Delete a line segment in the spatial index based on start/end points. To minimise the number of feature returned we search for a very small bounding box located in the middle of the line segment. Usually only one line segment is returned. :param qgs_geom_id: Integer ID of the geometry :param qgs_pnt0 : QgsPoint start point of the target line segment. :param qgs_pnt1 : QgsPoint end point of the target line segment. """ qgs_geom_to_delete = QgsGeometry(QgsLineString(qgs_pnt0, qgs_pnt1)) qgs_mid_point = QgsGeometryUtils.midpoint(qgs_pnt0, qgs_pnt1) qgs_rectangle = qgs_mid_point.boundingBox() qgs_rectangle.grow(Epsilon.ZERO_RELATIVE * 100) deleted = False ids = self._spatial_index.intersects(qgs_rectangle) for geom_id in ids: target_qgs_geom_id, target_qgs_geom = self._dict_qgs_segment[ geom_id] # Extract id and geometry if qgs_geom_id == target_qgs_geom_id: # Only check for the same ID if target_qgs_geom.equals( qgs_geom_to_delete): # Check if it's the same geometry deleted = True self._dict_qgs_segment[geom_id] = ( None, None) # Delete from the internal structure break if not deleted: raise Exception( QgsProcessingException("Internal structure corruption...")) return def _delete_vertex(self, rb_geom, v_id_start, v_id_end): """Delete consecutive vertex in the line and update the spatial index. When a vertex in a line string is deleted. Two line segments are deleted and one line segment is created in the spatial index. Cannot delete the first/last vertex of a line string :param rb_geom: LineString object to update. :param v_id_start: start of the vertex to delete. :param v_id_end: end of the vertex to delete. """ is_closed = rb_geom.qgs_geom.constGet().isClosed() v_ids_to_del = list(range(v_id_start, v_id_end + 1)) if v_id_start == 0 and is_closed: # Special case for closed line where we simulate a circular array nbr_vertice = rb_geom.qgs_geom.constGet().numPoints() v_ids_to_del.insert(0, nbr_vertice - 2) else: v_ids_to_del.insert(0, v_ids_to_del[0] - 1) v_ids_to_del.append(v_ids_to_del[-1] + 1) # Delete the line segment in the spatial index for i in range(len(v_ids_to_del) - 1): qgs_pnt0 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[i]) qgs_pnt1 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[i + 1]) self._delete_segment(rb_geom.id, qgs_pnt0, qgs_pnt1) # Add the new line segment in the spatial index qgs_pnt0 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[0]) qgs_pnt1 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[-1]) qgs_geom_segment = QgsGeometry(QgsLineString(qgs_pnt0, qgs_pnt1)) geom_id, qgs_rectangle = self._create_rectangle( rb_geom.id, qgs_geom_segment) self._spatial_index.addFeature(geom_id, qgs_rectangle) # Delete the vertex in the line string geometry for v_id_to_del in reversed(range(v_id_start, v_id_end + 1)): rb_geom.qgs_geom.deleteVertex(v_id_to_del) if v_id_start == 0 and is_closed: # Special case for closed line where we simulate a circular array nbr_vertice = rb_geom.qgs_geom.constGet().numPoints() qgs_pnt_first = rb_geom.qgs_geom.vertexAt(0) rb_geom.qgs_geom.insertVertex(qgs_pnt_first, nbr_vertice - 1) rb_geom.qgs_geom.deleteVertex(nbr_vertice) return def delete_vertex(self, rb_geom, v_id_start, v_id_end): """Manage deletion of consecutives vertex. If v_id_start is greater than v_id_end the delete is broken into up to 3 calls :param rb_geom: LineString object to update. :param v_id_start: start of the vertex to delete. :param v_id_end: end of the vertex to delete. """ num_points = rb_geom.qgs_geom.constGet().numPoints() # Manage closes line where first/last vertice are the same if v_id_start == num_points - 1: v_id_start = 0 # Last point is the same as the first vertice if v_id_end == -1: v_id_end = num_points - 2 # Preceding point the first/last vertice if v_id_start <= v_id_end: self._delete_vertex(rb_geom, v_id_start, v_id_end) else: self._delete_vertex(rb_geom, v_id_start, num_points - 2) self._delete_vertex(rb_geom, 0, 0) if v_id_end > 0: self._delete_vertex(rb_geom, 1, v_id_end) # lst_vertex_to_del = list(range(v_id_start, num_points)) + list(range(0, v_id_end+1)) # for vertex_to_del in lst_vertex_to_del: # self._delete_vertex(rb_geom, vertex_to_del, vertex_to_del) # num_points = rb_geom.qgs_geom.constGet().numPoints() # lst_vertex_to_del = list(range(v_id_start, num_points)) + list(range(0, v_id_end + 1)) # for vertex_to_del in lst_vertex_to_del: # self._delete_vertex(rb_geom, vertex_to_del, vertex_to_del) def add_vertex(self, rb_geom, bend_i, bend_j, qgs_geom_new_subline): """Update the line segment in the spatial index :param rb_geom: RbGeom line to update :param bend_i: Start of the bend to delete :param bend_j: End of the bend to delete (always bend_i + 1) :param qgs_geom_new_subline: New sub line string to add in the spatial index :return: """ # Delete the base of the bend qgs_pnt0 = rb_geom.qgs_geom.vertexAt(bend_i) qgs_pnt1 = rb_geom.qgs_geom.vertexAt(bend_j) self._delete_segment(rb_geom.id, qgs_pnt0, qgs_pnt1) qgs_points = qgs_geom_new_subline.constGet().points() tmp_qgs_points = qgs_points[1:-1] # Drop first/last item # Insert the new vertex in the QgsGeometry. Work reversely to facilitate insertion for qgs_point in reversed(tmp_qgs_points): rb_geom.qgs_geom.insertVertex(qgs_point, bend_j) # Add the new segment in the spatial container for i in range(len(qgs_points) - 1): qgs_geom_segment = QgsGeometry( QgsLineString(qgs_points[i], qgs_points[i + 1])) geom_id, qgs_rectangle = self._create_rectangle( rb_geom.id, qgs_geom_segment) self._spatial_index.addFeature(geom_id, qgs_rectangle) return def validate_integrity(self, rb_geoms): """This method is used to validate the data structure at the end of the process This method is executed only when requested and for debug purpose only. It's validating the data structure by removing element from it the data structure is unusable after. Validate integrity must be the last operation before ending the program as it destroy the data structure... :param rb_geoms: Geometry contained in the spatial container :return: Flag indicating if the structure is valid. True: is valid; False: is not valid :rtype: Boolean """ is_structure_valid = True # from the geometry remove all the segment in the spatial index. for rb_geom in rb_geoms: qgs_line_string = rb_geom.qgs_geom.constGet() if qgs_line_string.wkbType() == QgsWkbTypes.LineString: qgs_points = qgs_line_string.points() for i in range(len(qgs_points) - 1): self._delete_segment(rb_geom.id, qgs_points[i], qgs_points[i + 1]) if is_structure_valid: # Verify that there are no other feature in the spatial index; except for QgsPoint qgs_rectangle = QgsRectangle(-sys.float_info.max, -sys.float_info.max, sys.float_info.max, sys.float_info.max) feat_ids = self._spatial_index.intersects(qgs_rectangle) for feat_id in feat_ids: qgs_geom = self._spatial_index.geometry(feat_id) if qgs_geom.wkbType() == QgsWkbTypes.Point: pass else: # Error is_structure_valid = False return is_structure_valid
class TracingPipelines(QgsTask): def __init__(self, pipelines, valves, description='TracingCAJ', user_distance=0.001, onfinish=None, debug=False): super().__init__(description, QgsTask.CanCancel) self.onfinish = onfinish self.debug = debug self.__user_distance = user_distance self._pipelines_features = pipelines[0] self._valves_features = valves[0] self.__list_valves = [] self.__list_visited_pipelines = [] self.__list_visited_pipelines_ids = [] self.__q_list_pipelines = [] self.__q_list_pipelines_ids = [] self.__iterations = 0 self.__exception = None # Cria os índices espaciais self.__idx_pipelines = None self.__idx_valves = None if self.__idx_valves is None or self.__idx_pipelines is None: self.__create_spatial_index() def run(self): QgsMessageLog.logMessage(f'Started task {self.description()}', 'TracingCAJ', Qgis.Info) # Busca por redes selecionadas (necessário ser apenas uma) if self.debug: self._pipelines_features.selectByIds([4343]) # self._pipelines_features.getFeatures(16) selected_pipeline = self._pipelines_features.selectedFeatures() if len(selected_pipeline) != 1: QgsMessageLog.logMessage('Selecione apenas UMA rede', 'TracingCAJ', Qgis.Info) return False else: self.__q_list_pipelines.append(selected_pipeline[0].geometry()) self.__q_list_pipelines_ids.append(selected_pipeline[0].id()) while len(self.__q_list_pipelines) > 0: self.__iterations += 1 # check isCanceled() to handle cancellation if self.isCanceled(): return False pipeline = self.__q_list_pipelines.pop(0) pipeline_id = self.__q_list_pipelines_ids.pop(0) if pipeline_id not in self.__list_visited_pipelines_ids: self.__list_visited_pipelines.append(pipeline) self.__list_visited_pipelines_ids.append(pipeline_id) v1 = pipeline.vertexAt(0) if self.debug: v2 = pipeline.vertexAt(pipeline.get()[0].childCount() - 1) else: v2 = pipeline.vertexAt(len(pipeline.get()) - 1) try: self.__find_neighbors(v1) self.__find_neighbors(v2) except Exception as e: self.__exception = e return False return True def finished(self, result): if result: self._valves_features.selectByIds(self.__list_valves) self._pipelines_features.selectByIds( self.__list_visited_pipelines_ids) if self.onfinish: self.onfinish() QgsMessageLog.logMessage( f"Task {self.description()} has been executed correctly" f"Iterations: {self.__iterations}" f"Pipelines: {self.__list_visited_pipelines_ids}" f"Valves: {self.__list_valves}", level=Qgis.Success) else: if self.__exception is None: QgsMessageLog.logMessage( f"Tracing {self.description()} not successful " f"but without exception " f"(probably the task was manually canceled by the user)", level=Qgis.Warning) else: QgsMessageLog.logMessage( f"Task {self.description()}" f"Exception: {self.__exception}", level=Qgis.Critical) raise self.__exception def cancel(self): QgsMessageLog.logMessage( f'TracingTrask {self.description()} was canceled', level=Qgis.Info) super().cancel() def __create_spatial_index(self): self.__idx_pipelines = QgsSpatialIndex( self._pipelines_features.getFeatures(), flags=QgsSpatialIndex.FlagStoreFeatureGeometries) self.__idx_valves = QgsSpatialIndex( self._valves_features.getFeatures(), flags=QgsSpatialIndex.FlagStoreFeatureGeometries) def __find_neighbors(self, point_vertex): reg_isvisivel = None reg_status = None # Busca pelo registro mais próximo, dentro do raio maxDistance=user_distance reg_nearest = self.__idx_valves.nearestNeighbor( point=QgsPointXY(point_vertex), neighbors=1, maxDistance=self.__user_distance) if len(reg_nearest) > 0: # visivel = 'sim' = registro visível | visivel = 'não' = registro não visível reg_isvisivel = str( list(self._valves_features.getFeatures(reg_nearest))[0] ['visivel']) # status = 0 = 'Aberto' | status = 1 = 'Fechado' reg_status = str( list(self._valves_features.getFeatures(reg_nearest))[0] ['status']) if len(reg_nearest) > 0: if reg_isvisivel.upper() != 'NÃO' and reg_status == '0': self.__list_valves.append(reg_nearest[0]) else: # Busca pelas 4 redes mais próximas dentro do raio maxDistance=user_distance pipelines_nearest = self.__idx_pipelines.nearestNeighbor( point=QgsPointXY(point_vertex), neighbors=4, maxDistance=self.__user_distance) if len(pipelines_nearest) > 0: for pipeline_id in pipelines_nearest: pipeline_geometry = self.__idx_pipelines.geometry( pipeline_id) if pipeline_id not in self.__list_visited_pipelines_ids: self.__q_list_pipelines_ids.append(pipeline_id) self.__q_list_pipelines.append(pipeline_geometry)