diff --git a/controller.py b/controller.py index a56dea7..e96eba8 100644 --- a/controller.py +++ b/controller.py @@ -19,7 +19,7 @@ class FilterController(QObject): def __init__(self, parent: Optional[QObject] = None) -> None: super().__init__(parent=parent) self.currentFilter = FilterDefinition( - self.tr('New Filter'), '', QgsCoordinateReferenceSystem(), Predicate.INTERSECTS + self.tr('New Filter'), '', QgsCoordinateReferenceSystem(), Predicate.INTERSECTS, False ) self.rubberBands = [] self.toolbarIsActive = False @@ -88,6 +88,10 @@ def setFilterPredicate(self, predicate: Predicate): self.currentFilter.predicate = predicate.value self.refreshFilter() + def setFilterBbox(self, bbox: bool): + self.currentFilter.bbox = bbox + self.refreshFilter() + def saveCurrentFilter(self): FilterManager().saveFilterDefinition(self.currentFilter) self.refreshFilter() diff --git a/filters.py b/filters.py index ca1aeb2..7cc0ab3 100644 --- a/filters.py +++ b/filters.py @@ -23,6 +23,7 @@ class FilterDefinition: wkt: str crs: QgsCoordinateReferenceSystem predicate: int + bbox: bool def __post_init__(self): self.predicate = int(self.predicate) @@ -43,19 +44,25 @@ def filterString(self, layer: QgsVectorLayer) -> str: Returns: str: A layer filter string """ + template = "{spatial_predicate}({geom_name}, ST_TRANSFORM(ST_GeomFromText('{wkt}', {srid}), {layer_srid}))" # ST_DISJOINT does not use spatial indexes, but we can use its opposite "NOT ST_INTERSECTS" which does + spatial_predicate = f"ST_{Predicate(self.predicate).name}" if self.predicate == Predicate.DISJOINT: spatial_predicate = "NOT ST_INTERSECTS" - else: - spatial_predicate = f"ST_{Predicate(self.predicate).name}" - template = "{spatial_predicate}({geom_name}, ST_TRANSFORM(ST_GeomFromText('{wkt}', {srid}), {layer_srid}))" + + wkt = self.wkt + if self.bbox: + rect = QgsGeometry.fromWkt(self.wkt).boundingBox() + wkt = QgsGeometry.fromRect(rect).asWkt() + + geom_name = getLayerGeomName(layer) return template.format( spatial_predicate=spatial_predicate, geom_name=geom_name, - wkt=self.wkt, + wkt=wkt, srid=self.crs.postgisSrid(), layer_srid=layer.crs().postgisSrid() ) @@ -66,15 +73,16 @@ def storageString(self) -> str: For the CRS just the Auth ID is stored, e.g. EPSG:1234 or PROJ:9876. """ - return SPLIT_STRING.join([self.name, self.wkt, self.crs.authid(), str(self.predicate)]) + return SPLIT_STRING.join([self.name, self.wkt, self.crs.authid(), str(self.predicate), str(self.bbox)]) @staticmethod def fromStorageString(value: str) -> 'FilterDefinition': parameters = value.split(SPLIT_STRING) - assert len(parameters) == 4, "Malformed FilterDefinition loaded from settings: {value}" - name, wkt, crs_auth_id, predicate = parameters + assert len(parameters) == 5, "Malformed FilterDefinition loaded from settings: {value}" + name, wkt, crs_auth_id, predicate, bbox_str = parameters crs = QgsCoordinateReferenceSystem(crs_auth_id) - return FilterDefinition(name, wkt, crs, predicate) + bbox = bool(bbox_str == 'True') + return FilterDefinition(name, wkt, crs, predicate, bbox) @property def isValid(self) -> bool: diff --git a/widgets.py b/widgets.py index 0f278b2..e296ed7 100644 --- a/widgets.py +++ b/widgets.py @@ -181,6 +181,7 @@ def onNameClicked(self): class PredicateButton(QPushButton): predicateChanged = pyqtSignal(Predicate) + bboxChanged = pyqtSignal(bool) def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent=parent) @@ -188,6 +189,8 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self.setToolTip(self.tr('Geometric predicate')) self.setIcon(QgsApplication.getThemeIcon('/mActionOptions.svg')) self.menu = QMenu(parent=parent) + + self.menu.addSection(self.tr('Geometric Predicate')) self.predicateActionGroup = QActionGroup(self) self.predicateActionGroup.setExclusive(True) for predicate in Predicate: @@ -200,6 +203,29 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: action.triggered.connect(self.onPredicateChanged) self.predicateActionGroup.addAction(action) self.menu.addActions(self.predicateActionGroup.actions()) + self.menu.addSeparator() + + self.menu.addSection(self.tr('Object of comparison')) + self.bboxActionGroup = QActionGroup(self) + self.bboxActionGroup.setExclusive(True) + self.bboxTrueAction = QAction(self.menu) + self.bboxTrueAction.setCheckable(True) + self.bboxTrueAction.setText(self.tr('BBOX')) + self.bboxTrueAction.setToolTip(self.tr('Compare features to the filters bounding box')) + self.bboxTrueAction.bbox = True + self.bboxTrueAction.triggered.connect(self.onBboxChanged) + self.bboxFalseAction = QAction(self.menu) + self.bboxFalseAction.setCheckable(True) + self.bboxFalseAction.setChecked(True) + self.bboxFalseAction.setText(self.tr('GEOM')) + self.bboxFalseAction.setToolTip(self.tr('Compare features to the exact filter geometry')) + self.bboxFalseAction.bbox = False + self.bboxFalseAction.triggered.connect(self.onBboxChanged) + + self.bboxActionGroup.addAction(self.bboxTrueAction) + self.bboxActionGroup.addAction(self.bboxFalseAction) + self.menu.addActions(self.bboxActionGroup.actions()) + self.setMenu(self.menu) self.setFlat(True) @@ -211,13 +237,27 @@ def getPredicate(self) -> Predicate: if currentAction: return currentAction.predicate - def setCurrentAction(self, predicate: int): + def setCurrentPredicateAction(self, predicate: int): for action in self.predicateActionGroup.actions(): if action.predicate == Predicate(predicate): action.triggered.disconnect() action.setChecked(True) action.triggered.connect(self.onPredicateChanged) + def setCurrentBboxAction(self, bbox: bool): + action = self.bboxTrueAction if bbox else self.bboxFalseAction + action.triggered.disconnect() + action.setChecked(True) + action.triggered.connect(self.onBboxChanged) + + def onBboxChanged(self): + self.bboxChanged.emit(self.getBbox()) + + def getBbox(self): + currentAction = self.bboxActionGroup.checkedAction() + if currentAction: + return currentAction.bbox + class FilterToolbar(QToolBar): @@ -302,6 +342,7 @@ def setupConnections(self): self.manageFiltersAction.triggered.connect(self.startManageFiltersDialog) self.saveCurrentFilterAction.triggered.connect(self.controller.saveCurrentFilter) self.predicateButton.predicateChanged.connect(self.controller.setFilterPredicate) + self.predicateButton.bboxChanged.connect(self.controller.setFilterBbox) self.filterFromSelectionAction.triggered.connect(self.controller.setFilterFromSelection) self.controller.filterChanged.connect(self.onFilterChanged) self.toggleVisibilityAction.toggled.connect(self.onShowGeom) @@ -317,7 +358,8 @@ def onToggled(self, checked: bool): def onFilterChanged(self, filterDef: FilterDefinition): self.changeDisplayedName(filterDef) - self.predicateButton.setCurrentAction(filterDef.predicate) + self.predicateButton.setCurrentPredicateAction(filterDef.predicate) + self.predicateButton.setCurrentBboxAction(filterDef.bbox) self.onShowGeom(self.showGeomStatus) def changeDisplayedName(self, filterDef: FilterDefinition): @@ -359,8 +401,9 @@ def onShowGeom(self, checked: bool): def showFilterGeom(self): # Get filterRubberBand geometry, transform it and show it on canvas filterRubberBand = QgsRubberBand(iface.mapCanvas(), QgsWkbTypes.PolygonGeometry) - filterWkt = self.controller.currentFilter.wkt - filterGeom = QgsGeometry.fromWkt(filterWkt) + filterGeom = self.controller.currentFilter.geometry + if self.controller.currentFilter.bbox: + filterGeom = QgsGeometry.fromRect(filterGeom.boundingBox()) filterCrs = self.controller.currentFilter.crs projectCrs = QgsCoordinateReferenceSystem(QgsProject.instance().crs()) filterProj = QgsCoordinateTransform(filterCrs, projectCrs, QgsProject.instance())