From 948af1060fc2b62e03b5be02105ccb78308a3e60 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 13 Dec 2024 16:53:05 -0800 Subject: [PATCH 01/13] Gracefully handle if raster file is missing when adding to map --- src/QRiS/qris_map_manager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/QRiS/qris_map_manager.py b/src/QRiS/qris_map_manager.py index dbbf66b..a11f4e6 100644 --- a/src/QRiS/qris_map_manager.py +++ b/src/QRiS/qris_map_manager.py @@ -19,7 +19,9 @@ from .path_utilities import parse_posix_path +from qgis.utils import iface from qgis.core import ( + Qgis, QgsVectorLayer, QgsMapLayer, QgsProject, @@ -32,6 +34,7 @@ ) + class QRisMapManager(RiverscapesMapManager): def __init__(self, project: Project) -> None: @@ -281,6 +284,13 @@ def remove_pour_point_layers(self, pour_point: PourPoint) -> None: def build_raster_layer(self, raster: Raster) -> QgsMapLayer: + # check if raster file exists on disk + path = parse_posix_path(os.path.join(os.path.dirname(self.project.project_file), raster.path)) + if not os.path.exists(path): + # Warn user that the raster file is missing + iface.messageBar().pushMessage('Missing QRiS Raster File', f'The raster file {path} referenced in the QRiS project is missing.', level=Qgis.Warning) + return None + if raster.is_context is False: raster_machine_code = SURFACE_MACHINE_CODE group_layer_name = 'Surfaces' From dacdf1dcd449f758a8cf883941df7872e9e2373a Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 20 Dec 2024 14:23:45 -0800 Subject: [PATCH 02/13] migrate aois and valley bottoms to sample frames --- src/QRiS/qris_map_manager.py | 45 +++++--------- src/db/migrations/028_masks.sql | 58 ++++++++++++++++++ src/gp/copy_feature_class.py | 9 +-- src/gp/copy_raster.py | 4 +- src/gp/import_feature_class.py | 4 +- src/gp/import_temp_layer.py | 4 +- src/gp/metrics.py | 14 ++--- src/gp/metrics_task.py | 10 +-- src/model/mask.py | 83 ------------------------- src/model/project.py | 32 +++++----- src/model/sample_frame.py | 33 +++++++--- src/model/valley_bottom.py | 75 ----------------------- src/view/frm_basemap.py | 7 +-- src/view/frm_centerline_docwidget.py | 6 +- src/view/frm_cross_sections.py | 1 - src/view/frm_dockwidget.py | 91 ++++++++++++++-------------- src/view/frm_export_project.py | 70 +++++++++++---------- src/view/frm_geospatial_metrics.py | 10 +-- src/view/frm_mask_aoi.py | 64 ++++++++++--------- src/view/frm_profile.py | 2 +- src/view/frm_sample_frame.py | 3 +- src/view/frm_scratch_vector.py | 9 ++- src/view/frm_valley_bottom.py | 19 +++--- 23 files changed, 272 insertions(+), 381 deletions(-) create mode 100644 src/db/migrations/028_masks.sql delete mode 100644 src/model/mask.py delete mode 100644 src/model/valley_bottom.py diff --git a/src/QRiS/qris_map_manager.py b/src/QRiS/qris_map_manager.py index a11f4e6..9a70a62 100644 --- a/src/QRiS/qris_map_manager.py +++ b/src/QRiS/qris_map_manager.py @@ -5,8 +5,7 @@ from .riverscapes_map_manager import RiverscapesMapManager from ..model.project import Project, PROJECT_MACHINE_CODE -from ..model.mask import Mask, MASK_MACHINE_CODE, AOI_MACHINE_CODE, AOI_MASK_TYPE_ID -from ..model.sample_frame import SampleFrame, SAMPLE_FRAME_MACHINE_CODE +from ..model.sample_frame import SampleFrame, SAMPLE_FRAME_MACHINE_CODE, AOI_MACHINE_CODE, VALLEY_BOTTOM_MACHINE_CODE from ..model.stream_gage import StreamGage, STREAM_GAGE_MACHINE_CODE from ..model.scratch_vector import ScratchVector, SCRATCH_VECTOR_MACHINE_CODE from ..model.pour_point import PourPoint, CATCHMENTS_MACHINE_CODE @@ -15,7 +14,6 @@ from ..model.event_layer import EventLayer from ..model.profile import Profile from ..model.cross_sections import CrossSections -from ..model.valley_bottom import ValleyBottom from .path_utilities import parse_posix_path @@ -47,9 +45,8 @@ def __init__(self, project: Project) -> None: CrossSections.CROSS_SECTIONS_MACHINE_CODE, Profile.PROFILE_MACHINE_CODE, AOI_MACHINE_CODE, - ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE, - MASK_MACHINE_CODE, SAMPLE_FRAME_MACHINE_CODE, + VALLEY_BOTTOM_MACHINE_CODE, f'{EVENT_MACHINE_CODE}_ROOT', f'{DESIGN_MACHINE_CODE}_ROOT', STREAM_GAGE_MACHINE_CODE, @@ -59,42 +56,32 @@ def __init__(self, project: Project) -> None: 'QRiS Base Maps', BASEMAP_MACHINE_CODE] - def build_mask_layer(self, mask: Mask) -> QgsMapLayer: + def build_aoi_layer(self, aoi: SampleFrame) -> QgsMapLayer: - if mask.mask_type.id == AOI_MASK_TYPE_ID: - group_layer_name = 'AOIs' - mask_machine_code = AOI_MACHINE_CODE - symbology = 'mask' # TODO do aois need a different mask type? make the reference here... - layer_name = 'aoi_features' - else: - group_layer_name = 'Sample Frames' - mask_machine_code = MASK_MACHINE_CODE - symbology = 'sampling_frames' - layer_name = 'mask_features' + group_layer_name = 'AOIs' + mask_machine_code = AOI_MACHINE_CODE + symbology = 'mask' # TODO do aois need a different mask type? make the reference here... + layer_name = 'sample_frame_features' project_group = self.get_group_layer(self.project.map_guid, PROJECT_MACHINE_CODE, self.project.name, None, True) group_layer = self.get_group_layer(self.project.map_guid, mask_machine_code, group_layer_name, project_group, True) - existing_layer = self.get_db_item_layer(self.project.map_guid, mask, group_layer) + existing_layer = self.get_db_item_layer(self.project.map_guid, aoi, group_layer) if existing_layer is not None: return existing_layer fc_path = f'{self.project.project_file}|layername={layer_name}' - feature_layer = self.create_db_item_feature_layer(self.project.map_guid, group_layer, fc_path, mask, 'mask_id', symbology) + feature_layer = self.create_db_item_feature_layer(self.project.map_guid, group_layer, fc_path, aoi, 'sample_frame_id', symbology) # setup fields - self.set_hidden(feature_layer, 'fid', 'Mask Feature ID') - self.set_hidden(feature_layer, 'mask_id', 'Mask ID') + self.set_hidden(feature_layer, 'fid', 'AOI Feature ID') + self.set_hidden(feature_layer, 'sample_frame_id', 'AOI ID') self.set_alias(feature_layer, 'position', 'Position') self.set_multiline(feature_layer, 'description', 'Description') self.set_hidden(feature_layer, 'metadata', 'Metadata') self.set_virtual_dimension(feature_layer, 'area') self.set_metadata_virtual_fields(feature_layer) - if not mask.mask_type.id == AOI_MASK_TYPE_ID: - feature_layer.setLabelsEnabled(True) - feature_layer.setCustomProperty("labeling/fieldName", 'display_label') - return feature_layer def build_sample_frame_layer(self, sample_frame: SampleFrame) -> QgsMapLayer: @@ -172,21 +159,21 @@ def build_cross_section_layer(self, cross_sections: CrossSections) -> QgsMapLaye return feature_layer - def build_valley_bottom_layer(self, valley_bottom: ValleyBottom) -> QgsMapLayer: + def build_valley_bottom_layer(self, valley_bottom: SampleFrame) -> QgsMapLayer: project_group = self.get_group_layer(self.project.map_guid, PROJECT_MACHINE_CODE, self.project.name, None, True) - group_layer = self.get_group_layer(self.project.map_guid, ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE, 'Valley Bottoms', project_group, True) + group_layer = self.get_group_layer(self.project.map_guid, VALLEY_BOTTOM_MACHINE_CODE, 'Valley Bottoms', project_group, True) existing_layer = self.get_db_item_layer(self.project.map_guid, valley_bottom, group_layer) if existing_layer is not None: return existing_layer - fc_path = f'{self.project.project_file}|layername=valley_bottom_features' - feature_layer = self.create_db_item_feature_layer(self.project.map_guid, group_layer, fc_path, valley_bottom, 'valley_bottom_id', 'valley_bottom') + fc_path = f'{self.project.project_file}|layername=sample_frame_features' + feature_layer = self.create_db_item_feature_layer(self.project.map_guid, group_layer, fc_path, valley_bottom, 'sample_frame_id', 'valley_bottom') # setup fields self.set_hidden(feature_layer, 'fid', 'Valley Bottom Feature ID') - self.set_hidden(feature_layer, 'valley_bottom_id', 'Valley Bottom ID') + self.set_hidden(feature_layer, 'sample_frame_id', 'Valley Bottom ID') self.set_multiline(feature_layer, 'description', 'Description') self.set_hidden(feature_layer, 'metadata', 'Metadata') self.set_virtual_dimension(feature_layer, 'area') diff --git a/src/db/migrations/028_masks.sql b/src/db/migrations/028_masks.sql new file mode 100644 index 0000000..650fcb9 --- /dev/null +++ b/src/db/migrations/028_masks.sql @@ -0,0 +1,58 @@ +-- sqlite + +-- Create new sample frame types table +CREATE TABLE sample_frame_types ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + name TEXT NOT NULL, + description TEXT, + metadata TEXT, + created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +-- Add the new sample frame types +INSERT INTO sample_frame_types (id, name, description, metadata) VALUES (1, 'sample_frame', 'Sample Frame', '{}'); +INSERT INTO sample_frame_types (id, name, description, metadata) VALUES (2, 'aoi', 'Area of Interest', '{}'); +INSERT INTO sample_frame_types (id, name, description, metadata) VALUES (3, 'valley_bottom', 'Valley Bottom', '{}'); + +-- Add sample_frame_type_id to sample frames that references the sample_frame_types table +ALTER TABLE sample_frames ADD COLUMN sample_frame_type_id INTEGER REFERENCES sample_frame_types(id); + +-- the current sample frames are all sample_frame type +UPDATE sample_frames SET sample_frame_type_id = 1; + +-- copy the aois from the masks table to the sample_frames table +INSERT INTO sample_frames (name, description, metadata, sample_frame_type_id) + SELECT name, description, json_set(IFNULL(metadata, '{}'), '$.old_primary_key', id), 2 FROM masks; + +-- copy the features from aoi_features to sample_frame_features, and update the sample_frame_id to the new sample_frame_id +INSERT INTO sample_frame_features (geom, sample_frame_id, metadata) + SELECT AOIF.geom, SF.id, AOIF.metadata + FROM aoi_features AOIF + INNER JOIN sample_frames SF ON AOIF.mask_id = json_extract(SF.metadata, '$.old_primary_key'); + +-- drop the old primary key from the metadata +UPDATE sample_frames SET metadata = json_remove(metadata, '$.old_primary_key'); + +-- repeat for valley bottoms +INSERT INTO sample_frames (name, description, metadata, sample_frame_type_id) + SELECT name, description, json_set(IFNULL(metadata, '{}'), '$.old_primary_key', id), 3 FROM valley_bottoms; + +-- copy the features from valley_bottom_features to sample_frame_features, and update the sample_frame_id to the new sample_frame_id +INSERT INTO sample_frame_features (geom, sample_frame_id, metadata) + SELECT VBF.geom, SF.id, VBF.metadata + FROM valley_bottom_features VBF + INNER JOIN sample_frames SF ON VBF.valley_bottom_id = json_extract(SF.metadata, '$.old_primary_key'); + +-- drop the old primary key from the metadata +UPDATE sample_frames SET metadata = json_remove(metadata, '$.old_primary_key'); + +-- drop the tables +DROP TABLE valley_bottom_features; +DROP TABLE valley_bottoms; +DROP TABLE mask_features; +DROP TABLE aoi_features; +DROP TABLE masks; + +-- remove the entires for gpkg_contents and gpkg_geometry_columns +DELETE FROM gpkg_geometry_columns WHERE table_name IN ('aoi_features', 'mask_features', 'valley_bottom_features'); +DELETE FROM gpkg_contents WHERE table_name IN ('aoi_features', 'masks', 'valley_bottom_features', 'valley_bottoms', 'mask_features'); diff --git a/src/gp/copy_feature_class.py b/src/gp/copy_feature_class.py index a20d5dd..6714b29 100644 --- a/src/gp/copy_feature_class.py +++ b/src/gp/copy_feature_class.py @@ -35,11 +35,6 @@ def run(self): https://subscription.packtpub.com/book/application-development/9781787124837/3/ch03lvl1sec58/exporting-a-layer-to-the-geopackage-format """ - # if self.mask_tuple is not None: - # kwargs['cutlineDSName'] = self.mask_tuple[0] - # kwargs['cutlineLayer'] = 'mask_features' - # kwargs['cutlineWhere'] = 'mask_id = {}'.format(self.mask_tuple[1]) - self.setProgress(0) try: @@ -70,8 +65,8 @@ def run(self): if self.mask_tuple is not None: clip_path = self.mask_tuple[0] clip_mask_id = self.mask_tuple[1] - clip_layer = QgsVectorLayer(f'{clip_path}|layername=aoi_features') - clip_layer.setSubsetString(f'mask_id = {clip_mask_id}') + clip_layer = QgsVectorLayer(f'{clip_path}|layername=sample_frame_features') + clip_layer.setSubsetString(f'sample_frame_id = {clip_mask_id}') clip_transform = QgsCoordinateTransform(clip_layer.sourceCrs(), source_layer.sourceCrs(), QgsProject.instance().transformContext()) clip_feat = clip_layer.getFeatures() clip_feat = next(clip_feat) diff --git a/src/gp/copy_raster.py b/src/gp/copy_raster.py index 78cecac..6d2f29b 100644 --- a/src/gp/copy_raster.py +++ b/src/gp/copy_raster.py @@ -44,8 +44,8 @@ def run(self): if self.mask_tuple is not None: kwargs['cutlineDSName'] = self.mask_tuple[0] - kwargs['cutlineLayer'] = 'aoi_features' - kwargs['cutlineWhere'] = 'mask_id = {}'.format(self.mask_tuple[1]) + kwargs['cutlineLayer'] = 'sample_frame_features' + kwargs['cutlineWhere'] = 'sample_frame_id = {}'.format(self.mask_tuple[1]) kwargs['cropToCutline'] = True QgsMessageLog.logMessage(f'Started copy raster request', MESSAGE_CATEGORY, Qgis.Info) diff --git a/src/gp/import_feature_class.py b/src/gp/import_feature_class.py index a0d459e..bb33406 100644 --- a/src/gp/import_feature_class.py +++ b/src/gp/import_feature_class.py @@ -100,8 +100,8 @@ def run(self): mask_dataset = ogr.Open(self.proj_gpkg) else: mask_dataset = dst_dataset - clip_layer: ogr.Layer = mask_dataset.GetLayer(self.clip_mask[0]) # 'aoi_features' - clip_layer.SetAttributeFilter(f'{self.clip_mask[1]} = {self.clip_mask[2]}') # 'mask_id' + clip_layer: ogr.Layer = mask_dataset.GetLayer(self.clip_mask[0]) + clip_layer.SetAttributeFilter(f'{self.clip_mask[1]} = {self.clip_mask[2]}') # Gather all of the geoms and merge into a multipart geometry clip_geom = ogr.Geometry(ogr.wkbMultiPolygon) for clip_feat in clip_layer: diff --git a/src/gp/import_temp_layer.py b/src/gp/import_temp_layer.py index 9da0eba..93aab3e 100644 --- a/src/gp/import_temp_layer.py +++ b/src/gp/import_temp_layer.py @@ -80,8 +80,8 @@ def run(self): self.source_layer.updateFields() if self.clip_mask is not None: - clip_layer = QgsVectorLayer(f'{self.proj_gpkg}|layername={self.clip_mask[0]}') # aoi_features - clip_layer.setSubsetString(f'{self.clip_mask[1]} = {self.clip_mask[2]}') # mask_id + clip_layer = QgsVectorLayer(f'{self.proj_gpkg}|layername={self.clip_mask[0]}') + clip_layer.setSubsetString(f'{self.clip_mask[1]} = {self.clip_mask[2]}') clip_transform = QgsCoordinateTransform(clip_layer.sourceCrs(), self.source_layer.sourceCrs(), QgsProject.instance().transformContext()) clip_feat = clip_layer.getFeatures() clip_feat = next(clip_feat) diff --git a/src/gp/metrics.py b/src/gp/metrics.py index fb66485..f545564 100644 --- a/src/gp/metrics.py +++ b/src/gp/metrics.py @@ -8,29 +8,29 @@ from osgeo import gdal from shapely.geometry import mapping -from ..model.mask import Mask +from ..model.sample_frame import SampleFrame from .zonal_statistics import zonal_statistics class Metrics: - def __init__(self, project_file: str, mask_id: int, layers: list, mask_layer='mask_features'): + def __init__(self, project_file: str, mask_id: int, layers: list, aoi_layer='sample_frame_features'): self.config = {'vector': {}, 'raster': {}} self.project_file = project_file self.mask_id = mask_id self.layers = layers - self.mask_layer = mask_layer + self.mask_layer = aoi_layer self.metrics = {} self.polygons, self.utm_epsg = self.load_polygons(project_file, mask_id, self.mask_layer) # print(json.dumps(mapping(self.polygons[2]['geometry']))) - def load_polygons(self, project_file: str, mask: Mask, mask_layer: str = 'mask_features') -> dict: + def load_polygons(self, project_file: str, aoi: SampleFrame, aoi_layer: str = 'sample_frame_features') -> dict: driver = ogr.GetDriverByName("GPKG") ds = driver.Open(project_file) - layer = ds.GetLayerByName(mask_layer) - layer.SetAttributeFilter(f'mask_id = {mask.id}') + layer = ds.GetLayerByName(aoi_layer) + layer.SetAttributeFilter(f'sample_frame_id = {aoi.id}') src_srs = layer.GetSpatialRef() # Target transform to most appropriate UTM zone @@ -62,7 +62,7 @@ def load_polygons(self, project_file: str, mask: Mask, mask_layer: str = 'mask_f geom.MakeValid() polygons[feature.GetFID()] = { 'geometry': wkbload(bytes(geom.ExportToWkb())), - 'display_label': feature.GetField('display_label') if mask_layer == 'mask_features' else f'AOI {mask.name}' + 'display_label': feature.GetField('display_label') if aoi.sample_frame_type == SampleFrame.SAMPLE_FRAME_TYPE else f'AOI {aoi.name}' } layer = None diff --git a/src/gp/metrics_task.py b/src/gp/metrics_task.py index 3df28d5..fdcecec 100644 --- a/src/gp/metrics_task.py +++ b/src/gp/metrics_task.py @@ -5,7 +5,7 @@ from qgis.PyQt.QtCore import pyqtSignal from ..model.project import Project -from ..model.mask import Mask, AOI_MASK_TYPE_ID +from ..model.sample_frame import SampleFrame from .metrics import Metrics from ..model.raster import SURFACES_PARENT_FOLDER from ..QRiS.qris_map_manager import QRisMapManager @@ -21,9 +21,9 @@ class MetricsTask(QgsTask): """ # Signal to notify when done and return the PourPoint and whether it should be added to the map - on_complete = pyqtSignal(bool, Mask, dict or None, dict or None) + on_complete = pyqtSignal(bool, SampleFrame, dict or None, dict or None) - def __init__(self, project: Project, mask: Mask): + def __init__(self, project: Project, mask: SampleFrame): super().__init__(MESSAGE_CATEGORY, QgsTask.CanCancel) self.polygons = {} @@ -38,7 +38,7 @@ def __init__(self, project: Project, mask: Mask): # Skip the mask being used to summarize layers prop = layer_node.customProperty('QRiS') - if prop is not None and isinstance(mask, Mask) and prop == mask_guid: + if prop is not None and isinstance(mask, SampleFrame) and prop == mask_guid: continue layer_def = {'name': layer.name(), 'url': layer.dataProvider().dataSourceUri()} @@ -53,7 +53,7 @@ def __init__(self, project: Project, mask: Mask): self.project = project self.mask = mask self.config = {} - mask_layer = 'aoi_features' if self.mask.mask_type.id == AOI_MASK_TYPE_ID else 'mask_features' + mask_layer = 'sample_frame_features' self.metrics = Metrics(project.project_file, mask, self.map_layers, mask_layer) def run(self): diff --git a/src/model/mask.py b/src/model/mask.py deleted file mode 100644 index 3a25a54..0000000 --- a/src/model/mask.py +++ /dev/null @@ -1,83 +0,0 @@ -import json -import sqlite3 -from typing import Dict - -from qgis.core import QgsVectorLayer - -from .db_item import DBItem - -MASK_MACHINE_CODE = 'Mask' -AOI_MACHINE_CODE = 'AOI' - -AOI_MASK_TYPE_ID = 1 -REGULAR_MASK_TYPE_ID = 2 -DIRECTIONAL_MASK_TYPE_ID = 3 - - -class Mask(DBItem): - - def __init__(self, id: int, name: str, mask_type: DBItem, description: str, metadata: dict = None): - super().__init__('masks', id, name) - self.description = description - self.mask_type = mask_type - self.metadata = metadata - self.icon = 'mask' if mask_type.id == AOI_MASK_TYPE_ID else 'mask_regular' - self.fc_name = 'aoi_features' - self.fc_id_column_name = 'mask_id' - - def feature_count(self, db_path: str) -> int: - temp_layer = QgsVectorLayer(f'{db_path}|layername={self.fc_name}|subset={self.fc_id_column_name} = {self.id}', 'temp', 'ogr') - return temp_layer.featureCount() - - - def update(self, db_path: str, name: str, description: str, metadata=None) -> None: - - description = description if len(description) > 0 else None - metadata_str = json.dumps(metadata) if metadata is not None else None - with sqlite3.connect(db_path) as conn: - try: - curs = conn.cursor() - curs.execute('UPDATE masks SET name = ?, description = ?, metadata = ? WHERE id = ?', [name, description, metadata_str, self.id]) - conn.commit() - - self.name = name - self.description = description - self.metadata = metadata - - except Exception as ex: - conn.rollback() - raise ex - - -def load_masks(curs: sqlite3.Cursor, mask_types: dict) -> dict: - - curs.execute("""SELECT * FROM masks""") - return {row['id']: Mask( - row['id'], - row['name'], - mask_types[row['mask_type_id']], - row['description'], - json.loads(row['metadata']) if row['metadata'] is not None else None - ) for row in curs.fetchall()} - - -def insert_mask(db_path: str, name: str, mask_type: DBItem, description: str, metadata=None) -> Mask: - - mask = None - description = description if len(description) > 0 else None - metadata_str = json.dumps(metadata) if metadata is not None else None - - with sqlite3.connect(db_path) as conn: - try: - curs = conn.cursor() - curs.execute('INSERT INTO masks (name, mask_type_id, description, metadata) VALUES (?, ?, ?, ?)', [name, mask_type.id, description, metadata_str]) - id = curs.lastrowid - mask = Mask(id, name, mask_type, description, metadata) - conn.commit() - - except Exception as ex: - mask = None - conn.rollback() - raise ex - - return mask diff --git a/src/model/project.py b/src/model/project.py index fd0aadc..7cd6537 100644 --- a/src/model/project.py +++ b/src/model/project.py @@ -1,11 +1,12 @@ import os import json import sqlite3 +from sqlite3 import Connection from qgis.core import Qgis, QgsVectorLayer, QgsField, QgsVectorFileWriter, QgsCoordinateTransformContext, QgsMessageLog +from qgis.utils import spatialite_connect from .analysis import Analysis, load_analyses -from .mask import Mask, load_masks from .sample_frame import SampleFrame, load_sample_frames from .layer import Layer, load_layers, load_non_method_layers from .method import Method, load as load_methods @@ -20,7 +21,6 @@ from .stream_gage import StreamGage, load_stream_gages from .profile import Profile, load_profiles from .cross_sections import CrossSections, load_cross_sections -from .valley_bottom import ValleyBottom, load_valley_bottoms from .units import load_units from .db_item import DBItem, dict_factory, load_lookup_table @@ -32,8 +32,8 @@ # all spatial layers # feature class, layer name, geometry project_layers = [ - ('aoi_features', 'AOI Features', 'Polygon'), - ('mask_features', 'Mask Features', 'Polygon'), + ('aoi_features', 'AOI Features', 'Polygon'), # Keep so the migration scripts can run + ('mask_features', 'Mask Features', 'Polygon'), # Keep so the migration scripts can run ('sample_frame_features', 'Sample Frame Features', 'Polygon'), ('pour_points', 'Pour Points', 'Point'), ('catchments', 'Catchments', 'Polygon'), @@ -41,7 +41,7 @@ ('profile_centerlines', 'Centerlines', 'Linestring'), ('profile_features', 'Profiles', 'Linestring'), ('cross_section_features', 'Cross Sections', 'Linestring'), - ('valley_bottom_features', 'Valley Bottoms', 'Polygon'), + ('valley_bottom_features', 'Valley Bottoms', 'Polygon'), # Keep so the migration scripts can run ('dce_points', 'DCE Points', 'Point'), ('dce_lines', 'DCE Lines', 'Linestring'), ('dce_polygons', 'DCE Polygons', 'Polygon') @@ -77,8 +77,7 @@ def __init__(self, project_file: str): lkp_tables.append('lkp_scratch_vector_types') self.lookup_tables = {table: load_lookup_table(curs, table) for table in lkp_tables} - self.masks = load_masks(curs, self.lookup_tables['lkp_mask_types']) - self.aois = load_masks(curs, self.lookup_tables['lkp_mask_types']) + self.aois = load_sample_frames(curs, sample_frame_type=SampleFrame.AOI_SAMPLE_FRAME_TYPE) self.sample_frames = load_sample_frames(curs) self.layers = load_layers(curs) self.non_method_layers = load_non_method_layers(curs) @@ -94,7 +93,7 @@ def __init__(self, project_file: str): self.stream_gages = load_stream_gages(curs) self.profiles = load_profiles(curs) self.cross_sections = load_cross_sections(curs) - self.valley_bottoms = load_valley_bottoms(curs) + self.valley_bottoms = load_sample_frames(curs, sample_frame_type=SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE) self.units = load_units(curs) @@ -112,10 +111,13 @@ def get_safe_file_name(self, raw_name: str, ext: str = None): def remove(self, db_item: DBItem): - if isinstance(db_item, Mask): - self.masks.pop(db_item.id) - elif isinstance(db_item, SampleFrame): - self.sample_frames.pop(db_item.id) + if isinstance(db_item, SampleFrame): + if db_item.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: + self.aois.pop(db_item.id) + elif db_item.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + self.valley_bottoms.pop(db_item.id) + else: + self.sample_frames.pop(db_item.id) elif isinstance(db_item, Raster): self.rasters.pop(db_item.id) elif isinstance(db_item, Event): @@ -130,8 +132,6 @@ def remove(self, db_item: DBItem): self.profiles.pop(db_item.id) elif isinstance(db_item, CrossSections): self.cross_sections.pop(db_item.id) - elif isinstance(db_item, ValleyBottom): - self.valley_bottoms.pop(db_item.id) elif isinstance(db_item, EventLayer): event_layer_index = list(event_layer.id for event_layer in self.events[db_item.event_id].event_layers).index(db_item.id) self.events[db_item.event_id].event_layers.pop(event_layer_index) @@ -221,8 +221,9 @@ def create_geopackage_table(geometry_type: str, table_name: str, geopackage_path def apply_db_migrations(db_path: str): - conn = sqlite3.connect(db_path) + conn: Connection = spatialite_connect(db_path) conn.execute('PRAGMA foreign_keys = ON;') + conn.execute('SELECT EnableGpkgMode();') curs = conn.cursor() existing_layers = [layer[0] for layer in curs.execute('SELECT table_name, data_type FROM gpkg_contents WHERE data_type = "features"').fetchall()] @@ -251,7 +252,6 @@ def apply_db_migrations(db_path: str): except Exception as ex: conn.rollback() raise ex - # raise Exception('Error applying migration from file {}'.format(os.path.basename(migration_path)), inner=ex) conn.commit() except Exception as ex: diff --git a/src/model/sample_frame.py b/src/model/sample_frame.py index e469883..c345b81 100644 --- a/src/model/sample_frame.py +++ b/src/model/sample_frame.py @@ -6,23 +6,36 @@ from .db_item import DBItem -SAMPLE_FRAME_MACHINE_CODE = 'SampleFrame' +SAMPLE_FRAME_MACHINE_CODE = 'Sample Frame' +AOI_MACHINE_CODE = 'AOI' +VALLEY_BOTTOM_MACHINE_CODE = 'Valley Bottom' class SampleFrame(DBItem): - def __init__(self, id: int, name: str, description: str, metadata: dict = None): + SAMPLE_FRAME_TYPE = 1 + AOI_SAMPLE_FRAME_TYPE = 2 + VALLEY_BOTTOM_SAMPLE_FRAME_TYPE = 3 + + def __init__(self, id: int, name: str, description: str, metadata: dict = None, sample_frame_type=SAMPLE_FRAME_TYPE): super().__init__('sample_frames', id, name) self.description = description self.metadata = metadata self.user_metadata = None self.fields = None self.default_flow_path_name = None + self.sample_frame_type = sample_frame_type if metadata is not None: self.fields = metadata.get('fields', None) self.default_flow_path_name = metadata.get('default_flow_path_name', None) self.user_metadata = metadata.get('metadata', None) - self.icon = 'mask_regular' + if self.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: + self.icon = 'mask' + elif self.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + self.icon = 'valley_bottom' + else: + self.icon = 'mask_regular' + self.fc_name = 'sample_frame_features' self.fc_id_column_name = 'sample_frame_id' @@ -54,29 +67,31 @@ def update(self, db_path: str, name: str, description: str, metadata: dict=None) raise ex -def load_sample_frames(curs: sqlite3.Cursor) -> dict: +def load_sample_frames(curs: sqlite3.Cursor, sample_frame_type=SampleFrame.SAMPLE_FRAME_TYPE) -> dict: - curs.execute("""SELECT * FROM sample_frames""") + curs.execute("""SELECT * FROM sample_frames WHERE sample_frame_type_id = ?""", [sample_frame_type]) return {row['id']: SampleFrame( row['id'], row['name'], row['description'], - json.loads(row['metadata']) if row['metadata'] is not None else None + json.loads(row['metadata']) if row['metadata'] is not None else None, + row['sample_frame_type_id'] ) for row in curs.fetchall()} -def insert_sample_frame(db_path: str, name: str, description: str, metadata: dict=None) -> SampleFrame: +def insert_sample_frame(db_path: str, name: str, description: str, metadata: dict=None, sample_frame_type=SampleFrame.SAMPLE_FRAME_TYPE) -> SampleFrame: sample_frame = None + sample_frame_type = sample_frame_type description = description if len(description) > 0 else None metadata_str = json.dumps(metadata) if metadata is not None else None with sqlite3.connect(db_path) as conn: try: curs = conn.cursor() - curs.execute('INSERT INTO sample_frames (name, description, metadata) VALUES (?, ?, ?)', [name, description, metadata_str]) + curs.execute('INSERT INTO sample_frames (name, description, metadata, sample_frame_type_id) VALUES (?, ?, ?, ?)', [name, description, metadata_str, sample_frame_type]) id = curs.lastrowid - sample_frame = SampleFrame(id, name, description, metadata) + sample_frame = SampleFrame(id, name, description, metadata, sample_frame_type) conn.commit() except Exception as ex: diff --git a/src/model/valley_bottom.py b/src/model/valley_bottom.py deleted file mode 100644 index 5872722..0000000 --- a/src/model/valley_bottom.py +++ /dev/null @@ -1,75 +0,0 @@ -import json -import sqlite3 - -from qgis.core import QgsVectorLayer - -from .db_item import DBItem - - -class ValleyBottom(DBItem): - """ class to store valley bottom database item""" - - VALLEY_BOTTOM_MACHINE_CODE = 'Valley Bottom' - - def __init__(self, id: int, name: str, description: str, metadata: dict = None): - super().__init__('valley_bottoms', id, name) - self.description = description - self.metadata = metadata - self.icon = 'valley_bottom' - self.fc_name = 'valley_bottom_features' - self.fc_id_column_name = 'valley_bottom_id' - - def feature_count(self, db_path: str) -> int: - temp_layer = QgsVectorLayer(f'{db_path}|layername={self.fc_name}|subset={self.fc_id_column_name} = {self.id}', 'temp', 'ogr') - return temp_layer.featureCount() - - - def update(self, db_path: str, name: str, description: str, metadata: dict = None) -> None: - - description = description if len(description) > 0 else None - metadata_str = json.dumps(metadata) if metadata is not None else None - - with sqlite3.connect(db_path) as conn: - try: - curs = conn.cursor() - curs.execute('UPDATE valley_bottoms SET name = ?, description = ?, metadata = ? WHERE id = ?', [name, description, metadata_str, self.id, ]) - conn.commit() - - self.name = name - self.description = description - self.metadata = metadata - - except Exception as ex: - conn.rollback() - raise ex - - -def load_valley_bottoms(curs: sqlite3.Cursor) -> dict: - - curs.execute("""SELECT * FROM valley_bottoms""") - return {row['id']: ValleyBottom( - row['id'], - row['name'], - row['description'], - json.loads(row['metadata']) if row['metadata'] is not None else None - ) for row in curs.fetchall()} - - -def insert_valley_bottom(db_path: str, name: str, description: str, metadata: dict = None) -> ValleyBottom: - - valley_bottom = None - description = description if len(description) > 0 else None - metadata_str = json.dumps(metadata) if metadata is not None else None - with sqlite3.connect(db_path) as conn: - try: - curs = conn.cursor() - curs.execute('INSERT INTO valley_bottoms (name, description, metadata) VALUES (?, ?, ?)', [name, description, metadata_str]) - id = curs.lastrowid - valley_bottom = ValleyBottom(id, name, description, metadata) - conn.commit() - - except Exception as ex: - conn.rollback() - raise ex - - return valley_bottom diff --git a/src/view/frm_basemap.py b/src/view/frm_basemap.py index 8c46eb5..8ca7cb6 100644 --- a/src/view/frm_basemap.py +++ b/src/view/frm_basemap.py @@ -9,7 +9,6 @@ from ..model.raster import Raster, insert_raster, SURFACES_PARENT_FOLDER, CONTEXT_PARENT_FOLDER from ..model.db_item import DBItemModel, DBItem from ..model.project import Project -from ..model.mask import AOI_MASK_TYPE_ID from ..gp.copy_raster import CopyRaster from ..gp.create_hillshade import Hillshade @@ -83,10 +82,10 @@ def __init__(self, parent, iface, project: Project, import_source_path: str, is_ self.set_hillshade() # Masks (filtered to just AOI) - self.masks = {id: mask for id, mask in self.project.masks.items() if mask.mask_type.id == AOI_MASK_TYPE_ID} + self.clipping_masks = {id: aoi for id, aoi in self.project.aois.items()} no_clipping = DBItem('None', 0, 'None - Retain full dataset extent') - self.masks[0] = no_clipping - self.masks_model = DBItemModel(self.masks) + self.clipping_masks[0] = no_clipping + self.masks_model = DBItemModel(self.clipping_masks) self.cboMask.setModel(self.masks_model) # Default to no mask clipping self.cboMask.setCurrentIndex(self.masks_model.getItemIndex(no_clipping)) diff --git a/src/view/frm_centerline_docwidget.py b/src/view/frm_centerline_docwidget.py index 4e5dc8e..59c75fc 100644 --- a/src/view/frm_centerline_docwidget.py +++ b/src/view/frm_centerline_docwidget.py @@ -14,7 +14,6 @@ from ..model.db_item import DBItem from ..model.profile import Profile from ..model.layer import Layer -from ..model.mask import AOI_MASK_TYPE_ID from .frm_layer_picker import FrmLayerPicker from .capture_line_segment import LineSegmentMapTool @@ -115,8 +114,9 @@ def remove_preview_layers(self): def cmdSelectLayer_click(self): sv_layers = list(sv for sv in self.project.scratch_vectors.values() if QgsVectorLayer(f'{sv.gpkg_path}|layername={sv.fc_name}').geometryType() == Layer.GEOMETRY_TYPES['Polygon']) - aoi_layers = list(layer for layer in self.project.masks.values() if layer.mask_type.id == AOI_MASK_TYPE_ID) - layers = sv_layers + aoi_layers + aoi_layers = list(layer for layer in self.project.aois.values()) + valley_bottom_layers = list(layer for layer in self.project.valley_bottoms.values()) + layers = sv_layers + aoi_layers + valley_bottom_layers frm_layer_picker = FrmLayerPicker(self, "Select Polygon Layer", layers) result = frm_layer_picker.exec_() diff --git a/src/view/frm_cross_sections.py b/src/view/frm_cross_sections.py index 9211aa6..2978e14 100644 --- a/src/view/frm_cross_sections.py +++ b/src/view/frm_cross_sections.py @@ -7,7 +7,6 @@ from ..model.db_item import DBItem, DBItemModel from ..model.project import Project from ..model.cross_sections import CrossSections, insert_cross_sections -from ..model.mask import AOI_MASK_TYPE_ID from ..gp.feature_class_functions import import_existing, layer_path_parser from ..gp.import_temp_layer import ImportTemporaryLayer diff --git a/src/view/frm_dockwidget.py b/src/view/frm_dockwidget.py index 9979301..0d99f7c 100644 --- a/src/view/frm_dockwidget.py +++ b/src/view/frm_dockwidget.py @@ -40,8 +40,7 @@ from ..model.raster import BASEMAP_MACHINE_CODE, PROTOCOL_BASEMAP_MACHINE_CODE, SURFACE_MACHINE_CODE, Raster from ..model.analysis import ANALYSIS_MACHINE_CODE, Analysis from ..model.db_item import DB_MODE_NEW, DB_MODE_CREATE, DB_MODE_IMPORT, DB_MODE_IMPORT_TEMPORARY, DB_MODE_PROMOTE, DB_MODE_COPY, DBItem -from ..model.mask import AOI_MACHINE_CODE, AOI_MASK_TYPE_ID, Mask -from ..model.sample_frame import SAMPLE_FRAME_MACHINE_CODE, SampleFrame +from ..model.sample_frame import SAMPLE_FRAME_MACHINE_CODE, VALLEY_BOTTOM_MACHINE_CODE, AOI_MACHINE_CODE, SampleFrame from ..model.protocol import Protocol from ..model.method import Method from ..model.pour_point import PourPoint, CATCHMENTS_MACHINE_CODE @@ -49,14 +48,13 @@ from ..model.event_layer import EventLayer from ..model.profile import Profile from ..model.cross_sections import CrossSections -from ..model.valley_bottom import ValleyBottom from .frm_design2 import FrmDesign from .frm_event import DATA_CAPTURE_EVENT_TYPE_ID, FrmEvent from .frm_planning_container import FrmPlanningContainer from .frm_asbuilt import FrmAsBuilt from .frm_basemap import FrmRaster -from .frm_mask_aoi import FrmMaskAOI +from .frm_mask_aoi import FrmAOI from .frm_sample_frame import FrmSampleFrame from .frm_analysis_properties import FrmAnalysisProperties from .frm_analysis_explorer import FrmAnalysisExplorer @@ -105,7 +103,7 @@ # These are the labels used for displaying the group nodes in the QRiS project tree GROUP_FOLDER_LABELS = { INPUTS_NODE_TAG: 'Inputs', - ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE: 'Riverscapes', + VALLEY_BOTTOM_MACHINE_CODE: 'Riverscapes', SURFACE_MACHINE_CODE: 'Surfaces', AOI_MACHINE_CODE: 'AOIs', SAMPLE_FRAME_MACHINE_CODE: 'Sample Frames', @@ -190,14 +188,14 @@ def build_tree_view(self, project_file, new_item=None): project_node = self.add_child_to_project_tree(rootNode, self.project) inputs_node = self.add_child_to_project_tree(project_node, INPUTS_NODE_TAG) - riverscapes_node = self.add_child_to_project_tree(inputs_node, ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE) + riverscapes_node = self.add_child_to_project_tree(inputs_node, VALLEY_BOTTOM_MACHINE_CODE) [self.add_child_to_project_tree(riverscapes_node, item) for item in self.project.valley_bottoms.values()] surfaces_node = self.add_child_to_project_tree(inputs_node, SURFACE_MACHINE_CODE) [self.add_child_to_project_tree(surfaces_node, item) for item in self.project.rasters.values() if item.is_context is False] aoi_node = self.add_child_to_project_tree(inputs_node, AOI_MACHINE_CODE) - [self.add_child_to_project_tree(aoi_node, item) for item in self.project.aois.values() if item.mask_type.id == AOI_MASK_TYPE_ID] + [self.add_child_to_project_tree(aoi_node, item) for item in self.project.aois.values()] sample_frames_node = self.add_child_to_project_tree(inputs_node, SAMPLE_FRAME_MACHINE_CODE) [self.add_child_to_project_tree(sample_frames_node, item) for item in self.project.sample_frames.values()] @@ -391,7 +389,7 @@ def open_menu(self, position): self.add_context_menu_item(self.menu, 'Explore Stream Gages', 'refresh', lambda: self.stream_gage_explorer()) else: self.add_context_menu_item(self.menu, 'Add All Layers To The Map', 'add_to_map', lambda: self.add_tree_group_to_map(model_item)) - if all(model_data != data_type for data_type in [SURFACE_MACHINE_CODE, CONTEXT_NODE_TAG, CATCHMENTS_MACHINE_CODE, INPUTS_NODE_TAG, STREAM_GAGE_MACHINE_CODE, STREAM_GAGE_NODE_TAG, AOI_MACHINE_CODE, SAMPLE_FRAME_MACHINE_CODE, CLIMATE_ENGINE_MACHINE_CODE, Profile.PROFILE_MACHINE_CODE, CrossSections.CROSS_SECTIONS_MACHINE_CODE, ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE]): + if all(model_data != data_type for data_type in [SURFACE_MACHINE_CODE, CONTEXT_NODE_TAG, CATCHMENTS_MACHINE_CODE, INPUTS_NODE_TAG, STREAM_GAGE_MACHINE_CODE, STREAM_GAGE_NODE_TAG, AOI_MACHINE_CODE, SAMPLE_FRAME_MACHINE_CODE, CLIMATE_ENGINE_MACHINE_CODE, Profile.PROFILE_MACHINE_CODE, CrossSections.CROSS_SECTIONS_MACHINE_CODE, VALLEY_BOTTOM_MACHINE_CODE]): self.add_context_menu_item(self.menu, 'Add All Layers with Features To The Map', 'add_to_map', lambda: self.add_tree_group_to_map(model_item, True)) if model_data == EVENT_MACHINE_CODE: self.add_context_menu_item(self.menu, 'Add New Data Capture Event', 'new', lambda: self.add_event(model_item, DATA_CAPTURE_EVENT_TYPE_ID)) @@ -401,7 +399,7 @@ def open_menu(self, position): self.add_context_menu_item(ltpbr_menu, 'Add New Design', 'design', lambda: self.add_event(model_item, DESIGN_EVENT_TYPE_ID)) self.add_context_menu_item(ltpbr_menu, 'Add New As-Built Survey', 'as-built', lambda: self.add_event(model_item, AS_BUILT_EVENT_TYPE_ID)) self.menu.addMenu(ltpbr_menu) - elif model_data == ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE: + elif model_data == VALLEY_BOTTOM_MACHINE_CODE: import_menu = self.menu.addMenu('Import Valley Bottom From ... ') self.add_context_menu_item(import_menu, 'Existing Feature Class', 'new', lambda: self.add_valley_bottom(model_item, DB_MODE_IMPORT)) self.add_context_menu_item(import_menu, 'Temporary Layer', 'new', lambda: self.add_valley_bottom(model_item, DB_MODE_IMPORT_TEMPORARY)) @@ -410,9 +408,9 @@ def open_menu(self, position): self.add_context_menu_item(self.menu, 'Import Existing Raster Surface Dataset', 'new', lambda: self.add_raster(model_item, False)) elif model_data == AOI_MACHINE_CODE: import_menu = self.menu.addMenu('Import AOI From ... ') - self.add_context_menu_item(import_menu, 'Existing Feature Class', 'new', lambda: self.add_aoi(model_item, AOI_MASK_TYPE_ID, DB_MODE_IMPORT)) - self.add_context_menu_item(import_menu, 'Temporary Layer', 'new', lambda: self.add_aoi(model_item, AOI_MASK_TYPE_ID, DB_MODE_IMPORT_TEMPORARY)) - self.add_context_menu_item(self.menu, 'Create New (Manually Digitized) AOI', 'new', lambda: self.add_aoi(model_item, AOI_MASK_TYPE_ID, DB_MODE_CREATE)) + self.add_context_menu_item(import_menu, 'Existing Feature Class', 'new', lambda: self.add_aoi(model_item, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_IMPORT)) + self.add_context_menu_item(import_menu, 'Temporary Layer', 'new', lambda: self.add_aoi(model_item, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_IMPORT_TEMPORARY)) + self.add_context_menu_item(self.menu, 'Create New (Manually Digitized) AOI', 'new', lambda: self.add_aoi(model_item, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_CREATE)) elif model_data == SAMPLE_FRAME_MACHINE_CODE: import_sample_frame_menu = self.menu.addMenu('Import Sample Frame From ... ') self.add_context_menu_item(import_sample_frame_menu, 'Feature Class', 'new', lambda: self.add_sample_frame(model_item, DB_MODE_IMPORT)) @@ -462,17 +460,14 @@ def open_menu(self, position): else: raise Exception('Unhandled group folder clicked in QRiS project tree: {}'.format(model_data)) - if any(isinstance(model_data, model_type) for model_type in [Project, Event, Raster, Mask, SampleFrame, Profile, CrossSections, ValleyBottom, PourPoint, ScratchVector, Analysis, PlanningContainer]): + if any(isinstance(model_data, model_type) for model_type in [Project, Event, Raster, SampleFrame, Profile, CrossSections, PourPoint, ScratchVector, Analysis, PlanningContainer]): self.add_context_menu_item(self.menu, 'Properties', 'options', lambda: self.edit_item(model_item, model_data)) - if isinstance(model_data, ValleyBottom): - self.add_context_menu_item(self.menu, 'Generate Centerline', 'gis', lambda: self.generate_centerline(model_data)) - - if isinstance(model_data, Mask): - self.add_context_menu_item(self.menu, 'Zonal Statistics', 'gis', lambda: self.geospatial_summary(model_item, model_data)) - # if model_data.mask_type.id == AOI_MASK_TYPE_ID: - # self.add_context_menu_item(self.menu, 'Generate Centerline', 'gis', lambda: self.generate_centerline(model_data)) - # self.add_context_menu_item(self.menu, 'Generate Sample Frame', 'gis', lambda: self.add_sample_frame(model_data, DB_MODE_CREATE)) + if isinstance(model_data, SampleFrame): + if model_data.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + self.add_context_menu_item(self.menu, 'Generate Centerline', 'gis', lambda: self.generate_centerline(model_data)) + if model_data.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: + self.add_context_menu_item(self.menu, 'Zonal Statistics', 'gis', lambda: self.geospatial_summary(model_item, model_data)) if isinstance(model_data, Raster): # and model_data.raster_type_id != RASTER_TYPE_BASEMAP: self.add_context_menu_item(self.menu, 'Raster Slider', 'slider', lambda: self.raster_slider(model_data)) @@ -481,7 +476,7 @@ def open_menu(self, position): if QgsVectorLayer(f'{model_data.gpkg_path}|layername={model_data.fc_name}').geometryType() == QgsWkbTypes.PolygonGeometry: # self.add_context_menu_item(self.menu, 'Generate Centerline', 'gis', lambda: self.generate_centerline(model_data)) promote_menu = self.menu.addMenu('Promote to ...') - self.add_context_menu_item(promote_menu, 'AOI', 'mask', lambda: self.add_aoi(model_item, AOI_MASK_TYPE_ID, DB_MODE_PROMOTE)) + self.add_context_menu_item(promote_menu, 'AOI', 'mask', lambda: self.add_aoi(model_item, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_PROMOTE)) self.add_context_menu_item(promote_menu, 'Sample Frame', 'mask_regular', lambda: self.add_sample_frame(model_item, DB_MODE_PROMOTE)) self.add_context_menu_item(promote_menu, 'Riverscape Valley Bottom', 'polygon', lambda: self.add_valley_bottom(model_item, DB_MODE_PROMOTE)) if QgsVectorLayer(f'{model_data.gpkg_path}|layername={model_data.fc_name}').geometryType() == QgsWkbTypes.LineGeometry: @@ -520,7 +515,7 @@ def open_menu(self, position): if 'export_brat' in model_data.menu_items: self.add_context_menu_item(self.menu, 'Export BRAT CIS Obeservations...', 'save', lambda: self.export_brat_cis(model_data)) if isinstance(model_data, PourPoint): - self.add_context_menu_item(self.menu, 'Promote to AOI', 'mask', lambda: self.add_aoi(model_item, AOI_MASK_TYPE_ID, DB_MODE_PROMOTE), True) + self.add_context_menu_item(self.menu, 'Promote to AOI', 'mask', lambda: self.add_aoi(model_item, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_PROMOTE), True) if not isinstance(model_data, Project): # if an event is under a planning container node, then do not show the delete option @@ -673,10 +668,13 @@ def sort_children(self, tree_node: QtGui.QStandardItem, sort_key: str): def add_db_item_to_map(self, tree_node: QtGui.QStandardItem, db_item: DBItem): - if isinstance(db_item, Mask): - self.map_manager.build_mask_layer(db_item) - elif isinstance(db_item, SampleFrame): - self.map_manager.build_sample_frame_layer(db_item) + if isinstance(db_item, SampleFrame): + if db_item.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: + self.map_manager.build_aoi_layer(db_item) + elif db_item.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + self.map_manager.build_valley_bottom_layer(db_item) + else: + self.map_manager.build_sample_frame_layer(db_item) elif isinstance(db_item, Raster): self.map_manager.build_raster_layer(db_item) # find 'hillshade_raster_id' in metadata or system metadata and add to map if it exists @@ -708,7 +706,7 @@ def add_db_item_to_map(self, tree_node: QtGui.QStandardItem, db_item: DBItem): [self.map_manager.build_raster_layer(raster) for raster in event.rasters] elif isinstance(db_item, Project): [self.map_manager.build_sample_frame_layer(sample_frame) for sample_frame in self.project.sample_frames.values()] - [self.map_manager.build_mask_layer(mask) for mask in self.project.aois.values()] + [self.map_manager.build_aoi_layer(mask) for mask in self.project.aois.values()] [self.map_manager.build_raster_layer(raster) for raster in self.project.surface_rasters().values()] [[self.map_manager.build_event_single_layer(event, event_layer) for event_layer in event.event_layers] for event in self.project.events.values()] elif isinstance(db_item, PourPoint): @@ -719,8 +717,8 @@ def add_db_item_to_map(self, tree_node: QtGui.QStandardItem, db_item: DBItem): self.map_manager.build_profile_layer(db_item) elif isinstance(db_item, CrossSections): self.map_manager.build_cross_section_layer(db_item) - elif isinstance(db_item, ValleyBottom): - self.map_manager.build_valley_bottom_layer(db_item) + else: + iface.messageBar().pushMessage('Error', f'Unable to load qris data type: {type(db_item)} to the map', level=Qgis.Warning) def add_basemap_to_map(self, model_item, trigger_repaint=False): @@ -1032,8 +1030,8 @@ def copy_valley_bottom(self, db_item: DBItem): if frm.layer is None: return # now copy the valley bottom - valley_bottom_layer = QgsVectorLayer(f'{self.project.project_file}|layername=valley_bottom_features') - valley_bottom_layer.setSubsetString(f'valley_bottom_id = {frm.layer.id} ') + valley_bottom_layer = QgsVectorLayer(f'{self.project.project_file}|layername=sample_frame_features') + valley_bottom_layer.setSubsetString(f'sample_frame_id = {frm.layer.id} ') feats = [] out_layer = QgsVectorLayer(f'{self.project.project_file}|layername={Layer.DCE_LAYER_NAMES[db_item.layer.geom_type]}') new_fid = 1 if out_layer.featureCount() == 0 else max([f.id() for f in out_layer.getFeatures()]) + 1 @@ -1297,8 +1295,7 @@ def add_aoi(self, parent_node: QtGui.QStandardItem, mask_type_id: int, mode: int if import_source_path is None: return - if mask_type_id == AOI_MASK_TYPE_ID: - frm = FrmMaskAOI(self, self.project, import_source_path, self.project.lookup_tables['lkp_mask_types'][mask_type_id]) + frm = FrmAOI(self, self.project, import_source_path, self.project.lookup_tables['lkp_mask_types'][mask_type_id]) if mode == DB_MODE_PROMOTE: db_item = parent_node.data(QtCore.Qt.UserRole) @@ -1313,7 +1310,7 @@ def add_aoi(self, parent_node: QtGui.QStandardItem, mask_type_id: int, mode: int result = frm.exec_() if result != 0: - self.add_child_to_project_tree(parent_node, frm.qris_mask, frm.chkAddToMap.isChecked()) + self.add_child_to_project_tree(parent_node, frm.aoi, frm.chkAddToMap.isChecked()) def add_sample_frame(self, parent_node: QtGui.QStandardItem, mode: int): """Initiates adding a new sample frame""" @@ -1340,7 +1337,12 @@ def add_sample_frame(self, parent_node: QtGui.QStandardItem, mode: int): frm.promote_to_sample_frame(db_item) if mode == DB_MODE_CREATE: cross_sections = parent_node if isinstance(parent_node, CrossSections) else None - polygon = parent_node if any(isinstance(parent_node, item) for item in [Mask, ScratchVector]) else None + polygon = None + if isinstance(parent_node, ScratchVector): + polygon = parent_node + if isinstance(parent_node, SampleFrame): + if any(parent_node.sample_frame_type == item for item in [SampleFrame.AOI_SAMPLE_FRAME_TYPE, SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE]): + polygon = parent_node frm.set_inputs(cross_sections, polygon) if mode in [DB_MODE_CREATE, DB_MODE_PROMOTE]: # find the Sample Frames Node in the model @@ -1384,7 +1386,7 @@ def add_valley_bottom(self, parent_node: QtGui.QStandardItem, mode: int): rootNode = self.model.invisibleRootItem() project_node = self.add_child_to_project_tree(rootNode, self.project) inputs_node = self.add_child_to_project_tree(project_node, INPUTS_NODE_TAG) - riverscapes_node = self.add_child_to_project_tree(inputs_node, ValleyBottom.VALLEY_BOTTOM_MACHINE_CODE) + riverscapes_node = self.add_child_to_project_tree(inputs_node, VALLEY_BOTTOM_MACHINE_CODE) parent_node = riverscapes_node self.add_child_to_project_tree(parent_node, frm.valley_bottom, frm.chkAddToMap.isChecked()) @@ -1554,16 +1556,17 @@ def edit_item(self, model_item: QtGui.QStandardItem, db_item: DBItem): frm = FrmAsBuilt(self, self.project, db_item.event_type.id, event=db_item) else: frm = FrmEvent(self, self.project, event=db_item, event_type_id=db_item.event_type.id) - elif isinstance(db_item, Mask): - frm = FrmMaskAOI(self, self.project, None, db_item.mask_type, db_item) elif isinstance(db_item, SampleFrame): - frm = FrmSampleFrame(self, self.project, None, db_item) + if db_item.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: + frm = FrmAOI(self, self.project, None, db_item.sample_frame_type, db_item) + elif db_item.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + frm = FrmValleyBottom(self, self.project, None, db_item) + else: + frm = FrmSampleFrame(self, self.project, None, db_item) elif isinstance(db_item, Profile): frm = FrmProfile(self, self.project, None, db_item) elif isinstance(db_item, CrossSections): frm = FrmCrossSections(self, self.project, None, db_item) - elif isinstance(db_item, ValleyBottom): - frm = FrmValleyBottom(self, self.project, None, db_item) elif isinstance(db_item, Raster): frm = FrmRaster(self, self.iface, self.project, None, db_item.raster_type_id, db_item) elif isinstance(db_item, ScratchVector): @@ -1595,7 +1598,7 @@ def edit_item(self, model_item: QtGui.QStandardItem, db_item: DBItem): else: self.add_child_to_project_tree(model_item.parent(), db_item, frm.chkAddToMap.isChecked()) - def geospatial_summary(self, model_item, model_data: Mask): + def geospatial_summary(self, model_item, model_data: SampleFrame): # Check the feature count of the aoi, make sure there is one and only one polygon feature aoi_layer = QgsVectorLayer(f'{self.project.project_file}|layername={model_data.fc_name}|subset={model_data.fc_id_column_name} = {model_data.id}', 'aoi', 'ogr') @@ -1767,7 +1770,7 @@ def remove_empty_child_nodes(self, node: QtGui.QStandardItem): self.remove_empty_child_nodes(child_node) - @ pyqtSlot(bool, Mask, dict or None, dict or None) + @ pyqtSlot(bool, SampleFrame, dict or None, dict or None) def geospatial_summary_complete(self, result, model_data, polygons, data): if result is True: diff --git a/src/view/frm_export_project.py b/src/view/frm_export_project.py index 1aeef91..cc312b3 100644 --- a/src/view/frm_export_project.py +++ b/src/view/frm_export_project.py @@ -19,13 +19,11 @@ from ..model.profile import Profile from ..model.pour_point import PourPoint from ..model.cross_sections import CrossSections -from ..model.mask import Mask, AOI_MASK_TYPE_ID from ..model.project import Project as QRiSProject from ..model.raster import Raster from ..model.scratch_vector import ScratchVector, scratch_gpkg_path from ..model.stream_gage import StreamGage from ..model.sample_frame import SampleFrame -from ..model.valley_bottom import ValleyBottom from ..QRiS.path_utilities import parse_posix_path @@ -60,9 +58,8 @@ def __init__(self, parent, project: QRiSProject, outpath: str = None): self.set_output_path() # populate the AOI combo box with aoi names - for aoi_id, aoi in self.qris_project.masks.items(): - if aoi.mask_type.id == AOI_MASK_TYPE_ID: - self.cbo_project_bounds_aoi.addItem(aoi.name, aoi_id) + for aoi_id, aoi in self.qris_project.aois.items(): + self.cbo_project_bounds_aoi.addItem(aoi.name, aoi_id) # Inputs inputs_node = QtGui.QStandardItem("Inputs") @@ -85,13 +82,12 @@ def __init__(self, parent, project: QRiSProject, outpath: str = None): aois_node = QtGui.QStandardItem("AOIs") aois_node.setCheckable(True) aois_node.setCheckState(QtCore.Qt.Checked) - for aoi in self.qris_project.masks.values(): - if aoi.mask_type.id == AOI_MASK_TYPE_ID: - item = QtGui.QStandardItem(aoi.name) - item.setCheckable(True) - item.setCheckState(QtCore.Qt.Checked) - item.setData(aoi, QtCore.Qt.UserRole) - aois_node.appendRow(item) + for aoi in self.qris_project.aois.values(): + item = QtGui.QStandardItem(aoi.name) + item.setCheckable(True) + item.setCheckState(QtCore.Qt.Checked) + item.setData(aoi, QtCore.Qt.UserRole) + aois_node.appendRow(item) inputs_node.appendRow(aois_node) # Sample Frames @@ -375,9 +371,9 @@ def accept(self) -> None: else: # get the extent of the selected AOI aoi_id = self.cbo_project_bounds_aoi.currentData() - aoi: Mask = self.qris_project.masks[aoi_id] - lyr = QgsVectorLayer(f'{self.qris_project.project_file}|layername=aoi_features', aoi.name, "ogr") - lyr.setSubsetString(f"mask_id = {aoi.id}") + aoi: SampleFrame = self.qris_project.aois[aoi_id] + lyr = QgsVectorLayer(f'{self.qris_project.project_file}|layername=sample_frame_features', aoi.name, "ogr") + lyr.setSubsetString(f"sample_frame_id = {aoi.id}") envelope = lyr.getFeatures().__next__().geometry() if envelope is not None and not envelope.isNull() and not envelope.isEmpty(): @@ -479,19 +475,21 @@ def accept(self) -> None: valley_bottom_item = valley_bottom_node.child(i) if valley_bottom_item.checkState() == QtCore.Qt.Unchecked: continue - valley_bottom: ValleyBottom = valley_bottom_item.data(QtCore.Qt.UserRole) + valley_bottom: SampleFrame = valley_bottom_item.data(QtCore.Qt.UserRole) + if not valley_bottom.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + continue - if 'valley_bottom_features' not in keep_layers: - keep_layers['valley_bottom_features'] = {'id_field': 'valley_bottom_id', 'id_values': []} - keep_layers['valley_bottom_features']['id_values'].append(str(valley_bottom.id)) - if 'valley_bottoms' not in keep_layers: - keep_layers['valley_bottoms'] = {'id_field': 'id', 'id_values': []} - keep_layers['valley_bottoms']['id_values'].append(str(valley_bottom.id)) + if 'sample_frame_features' not in keep_layers: + keep_layers['sample_frame_features'] = {'id_field': 'sample_frame_id', 'id_values': []} + keep_layers['sample_frame_features']['id_values'].append(str(valley_bottom.id)) + if 'sample_frames' not in keep_layers: + keep_layers['sample_frames'] = {'id_field': 'id', 'id_values': []} + keep_layers['sample_frames']['id_values'].append(str(valley_bottom.id)) view_name = f'vw_valley_bottom_{valley_bottom.id}' self.create_spatial_view(view_name=view_name, - fc_name='valley_bottom_features', - field_name='valley_bottom_id', + fc_name='sample_frame_features', + field_name='sample_frame_id', id_value=valley_bottom.id, out_geopackage=out_geopackage, geom_type='POLYGON') @@ -509,21 +507,21 @@ def accept(self) -> None: if aoi_item.checkState() == QtCore.Qt.Unchecked: continue - aoi: Mask = aoi_item.data(QtCore.Qt.UserRole) - if not aoi.mask_type.id == AOI_MASK_TYPE_ID: + aoi: SampleFrame = aoi_item.data(QtCore.Qt.UserRole) + if not aoi.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: continue - if 'aoi_features' not in keep_layers: - keep_layers['aoi_features'] = {'id_field': 'mask_id', 'id_values': []} - keep_layers['aoi_features']['id_values'].append(str(aoi.id)) - if 'masks' not in keep_layers: - keep_layers['masks'] = {'id_field': 'id', 'id_values': []} - keep_layers['masks']['id_values'].append(str(aoi.id)) + if 'sample_frame_features' not in keep_layers: + keep_layers['sample_frame_features'] = {'id_field': 'sample_frame_id', 'id_values': []} + keep_layers['sample_frame_features']['id_values'].append(str(aoi.id)) + if 'sample_frames' not in keep_layers: + keep_layers['sample_frames'] = {'id_field': 'id', 'id_values': []} + keep_layers['sample_frames']['id_values'].append(str(aoi.id)) view_name = f'vw_aoi_{aoi.id}' self.create_spatial_view(view_name=view_name, - fc_name='aoi_features', - field_name='mask_id', + fc_name='sample_frame_features', + field_name='sample_frame_id', id_value=aoi.id, out_geopackage=out_geopackage, geom_type='POLYGON') @@ -912,7 +910,7 @@ def accept(self) -> None: geopackage_layers = [] # analysis: Analysis = analysis - sample_frame: Mask = analysis.sample_frame + sample_frame: SampleFrame = analysis.sample_frame # flatten the table of analysis metrics analysis_metrics = [] @@ -963,7 +961,7 @@ def accept(self) -> None: # open the geopackage using ogr ds_gpkg: ogr.DataSource = ogr.Open(out_geopackage, 1) - for layer in ['analyses','analysis_metrics', 'aoi_features', 'catchments', 'cross_sections', 'cross_section_features', 'dce_lines', 'dce_points', 'dce_polygons', 'events', 'event_layers','mask_features', 'masks', 'pour_points', 'profile_centerlines', 'profile_features', 'profiles', 'rasters', 'scratch_vectors', 'sample_frame_features','sample_frames','valley_bottom_features', 'valley_bottoms']: + for layer in ['analyses','analysis_metrics', 'catchments', 'cross_sections', 'cross_section_features', 'dce_lines', 'dce_points', 'dce_polygons', 'events', 'event_layers', 'pour_points', 'profile_centerlines', 'profile_features', 'profiles', 'rasters', 'scratch_vectors', 'sample_frame_features','sample_frames']: # get the layer lyr: ogr.Layer = ds_gpkg.GetLayerByName(layer) # remove all features that are not in the keep list diff --git a/src/view/frm_geospatial_metrics.py b/src/view/frm_geospatial_metrics.py index bd2a6a0..6ca9bd8 100644 --- a/src/view/frm_geospatial_metrics.py +++ b/src/view/frm_geospatial_metrics.py @@ -4,26 +4,26 @@ from .frm_options import FrmOptions from ..model.project import Project -from ..model.mask import Mask +from ..model.sample_frame import SampleFrame from .utilities import add_standard_form_buttons class FrmGeospatialMetrics(QtWidgets.QDialog): - def __init__(self, parent, project: Project, mask: Mask, polygons: dict, metrics: dict): + def __init__(self, parent, project: Project, mask: SampleFrame, polygons: dict, metrics: dict): super().__init__(parent) self.qris_project = project - self.mask = mask + self.qris_mask = mask self.metrics = metrics self.polygons = polygons self.setupUi() - self.setWindowTitle(f'Zonal Statistics for {self.mask.name}') + self.setWindowTitle(f'Zonal Statistics for {self.qris_mask.name}') - self.txtMask.setText(self.mask.name) + self.txtMask.setText(self.qris_mask.name) self.load_tree() diff --git a/src/view/frm_mask_aoi.py b/src/view/frm_mask_aoi.py index e180b26..671e395 100644 --- a/src/view/frm_mask_aoi.py +++ b/src/view/frm_mask_aoi.py @@ -6,7 +6,7 @@ from ..model.db_item import DBItem, DBItemModel from ..model.project import Project -from ..model.mask import Mask, insert_mask, REGULAR_MASK_TYPE_ID, AOI_MASK_TYPE_ID +from ..model.sample_frame import SampleFrame, insert_sample_frame from ..model.pour_point import PourPoint from ..model.scratch_vector import ScratchVector @@ -18,35 +18,34 @@ from .utilities import validate_name, add_standard_form_buttons -class FrmMaskAOI(QtWidgets.QDialog): +class FrmAOI(QtWidgets.QDialog): - def __init__(self, parent, project: Project, import_source_path: str, mask_type: DBItem, mask: Mask = None): + def __init__(self, parent, project: Project, import_source_path: str, aoi: SampleFrame = None): self.qris_project = project - self.qris_mask = mask + self.aoi = aoi self.import_source_path = import_source_path self.attribute_filter = None - self.mask_type = mask_type - self.str_mask_type = "AOI" if self.mask_type.id == AOI_MASK_TYPE_ID else "Sample Frame" + self.str_mask_type = "AOI" - super(FrmMaskAOI, self).__init__(parent) - metadata_json = json.dumps(mask.metadata) if mask is not None else None + super(FrmAOI, self).__init__(parent) + metadata_json = json.dumps(aoi.metadata) if aoi is not None else None self.metadata_widget = MetadataWidget(self, metadata_json) self.setupUi() - if self.qris_mask is not None: - self.setWindowTitle(f'Edit {mask_type.name} Properties') + if self.aoi is not None: + self.setWindowTitle(f'Edit AOI Properties') elif import_source_path is not None: - self.setWindowTitle(f'Import {mask_type.name} Features') + self.setWindowTitle(f'Import AOI Features') else: - self.setWindowTitle(f'Create New {mask_type.name}') + self.setWindowTitle(f'Create New AOI') # The attribute picker is only visible when creating a new regular mask - show_attribute_filter = mask_type.id == REGULAR_MASK_TYPE_ID + show_attribute_filter = True self.lblAttribute.setVisible(show_attribute_filter) self.cboAttribute.setVisible(show_attribute_filter) - show_mask_clip = import_source_path is not None and mask_type.id == REGULAR_MASK_TYPE_ID + show_mask_clip = False self.lblMaskClip.setVisible(show_mask_clip) self.cboMaskClip.setVisible(show_mask_clip) @@ -76,17 +75,17 @@ def __init__(self, parent, project: Project, import_source_path: str, mask_type: if show_mask_clip: # Masks (filtered to just AOI) - self.masks = {id: mask for id, mask in self.qris_project.masks.items() if mask.mask_type.id == AOI_MASK_TYPE_ID} + self.clipping_masks = {id: aoi for id, aoi in self.qris_project.aois.items()} no_clipping = DBItem('None', 0, 'None - Retain full dataset extent') - self.masks[0] = no_clipping - self.masks_model = DBItemModel(self.masks) + self.clipping_masks[0] = no_clipping + self.masks_model = DBItemModel(self.clipping_masks) self.cboMaskClip.setModel(self.masks_model) # Default to no mask clipping self.cboMaskClip.setCurrentIndex(self.masks_model.getItemIndex(no_clipping)) - if self.qris_mask is not None: - self.txtName.setText(mask.name) - self.txtDescription.setPlainText(mask.description) + if self.aoi is not None: + self.txtName.setText(aoi.name) + self.txtDescription.setPlainText(aoi.description) self.chkAddToMap.setCheckState(QtCore.Qt.Unchecked) self.chkAddToMap.setVisible(False) @@ -129,12 +128,11 @@ def accept(self): metadata = json.loads(metadata_json) if metadata_json is not None else None try: - if self.qris_mask is not None: - self.qris_mask.update(self.qris_project.project_file, self.txtName.text(), self.txtDescription.toPlainText(), metadata) + if self.aoi is not None: + self.aoi.update(self.qris_project.project_file, self.txtName.text(), self.txtDescription.toPlainText(), metadata) else: - self.qris_mask = insert_mask(self.qris_project.project_file, self.txtName.text(), self.mask_type, self.txtDescription.toPlainText(), metadata) - self.qris_project.masks[self.qris_mask.id] = self.qris_mask - self.qris_project.aois[self.qris_mask.id] = self.qris_mask + self.aoi = insert_sample_frame(self.qris_project.project_file, self.txtName.text(), self.txtDescription.toPlainText(), metadata, sample_frame_type=SampleFrame.AOI_SAMPLE_FRAME_TYPE) + self.qris_project.aois[self.aoi.id] = self.aoi except Exception as ex: if 'unique' in str(ex).lower(): QtWidgets.QMessageBox.warning(self, 'Duplicate Name', f"A {self.str_mask_type} with the name '{self.txtName.text()}' already exists. Please choose a unique name.") @@ -145,15 +143,15 @@ def accept(self): if self.import_source_path is not None: try: - mask_layer_name = "aoi_features" if self.mask_type.id == AOI_MASK_TYPE_ID else "mask_features" + mask_layer_name = "sample_frame_features" mask_path = f'{self.qris_project.project_file}|layername={mask_layer_name}' - layer_attributes = {'mask_id': self.qris_mask.id} + layer_attributes = {'sample_frame_id': self.aoi.id} field_map = [ImportFieldMap(self.cboAttribute.currentData(QtCore.Qt.UserRole).name, 'display_label', direct_copy=True)] if self.cboAttribute.isVisible() else None clip_mask = None clip_item = self.cboMaskClip.currentData(QtCore.Qt.UserRole) if clip_item is not None: if clip_item.id > 0: - clip_mask = ('aoi_features', 'mask_id', clip_item.id) + clip_mask = ('sample_frame_features', 'sample_frame_id', clip_item.id) if self.layer_id == 'memory': import_mask_task = ImportTemporaryLayer(self.import_source_path, mask_path, layer_attributes, field_map, clip_mask, self.attribute_filter, self.qris_project.project_file) @@ -167,7 +165,7 @@ def accept(self): QgsApplication.taskManager().addTask(import_mask_task) except Exception as ex: try: - self.qris_mask.delete(self.qris_project.project_file) + self.aoi.delete(self.qris_project.project_file) QgsApplication.messageLog().logMessage(f'Error Importing {self.str_mask_type}: {str(ex)}', 'QRIS', level=Qgis.Critical) iface.messageBar().pushMessage(f'Error Importing {self.str_mask_type}', str(ex), level=Qgis.Critical, duration=5) except Exception as ex_delete: @@ -175,7 +173,7 @@ def accept(self): iface.messageBar().pushMessage(f'Error Deleting {self.str_mask_type}', str(ex_delete), level=Qgis.Critical, duration=5) return else: - super(FrmMaskAOI, self).accept() + super(FrmAOI, self).accept() def on_import_complete(self, result: bool): @@ -184,12 +182,12 @@ def on_import_complete(self, result: bool): else: QgsApplication.messageLog().logMessage(f'Error Importing {self.str_mask_type} Features', 'QRIS', level=Qgis.Critical) try: - self.qris_mask.delete(self.qris_project.project_file) + self.aoi.delete(self.qris_project.project_file) except Exception as ex: QgsApplication.messageLog().logMessage(f'Error Deleting {self.str_mask_type}: {str(ex)}', 'QRIS', level=Qgis.Critical) iface.messageBar().pushMessage(f'Error Deleting {self.str_mask_type}', str(ex), level=Qgis.Critical, duration=5) return - super(FrmMaskAOI, self).accept() + super(FrmAOI, self).accept() def setupUi(self): @@ -244,5 +242,5 @@ def setupUi(self): self.chkAddToMap.setText('Add to Map') self.grid.addWidget(self.chkAddToMap, 4, 1, 1, 1) - help = 'aoi' if self.mask_type.id == AOI_MASK_TYPE_ID else 'sample-frames' + help = 'aoi' self.vert.addLayout(add_standard_form_buttons(self, help)) diff --git a/src/view/frm_profile.py b/src/view/frm_profile.py index dbbed39..c0eb5a8 100644 --- a/src/view/frm_profile.py +++ b/src/view/frm_profile.py @@ -131,7 +131,7 @@ def accept(self): clip_item = self.cboMaskClip.currentData(QtCore.Qt.UserRole) if clip_item is not None: if clip_item.id > 0: - clip_mask = ('aoi_features', 'mask_id', clip_item.id) + clip_mask = ('sample_frame_features', 'sample_frame_id', clip_item.id) if self.layer_id == 'memory': task = ImportTemporaryLayer(self.import_source_path, fc_path, {'profile_id': self.profile.id}, clip_mask=clip_mask, proj_gpkg=self.qris_project.project_file) # DEBUG task.run() diff --git a/src/view/frm_sample_frame.py b/src/view/frm_sample_frame.py index 1ca0f5c..60a0cc8 100644 --- a/src/view/frm_sample_frame.py +++ b/src/view/frm_sample_frame.py @@ -12,7 +12,6 @@ from ..model.db_item import DBItem, DBItemModel from ..model.pour_point import PourPoint from ..model.scratch_vector import ScratchVector -from ..model.mask import Mask, AOI_MASK_TYPE_ID from ..model.sample_frame import SampleFrame, insert_sample_frame from .frm_new_attribute import FrmNewAttribute @@ -173,7 +172,7 @@ def accept(self): clip_item = self.tab_inputs.cboClipToAOI.currentData(Qt.UserRole) if clip_item is not None: if clip_item.id > 0: - clip_mask = ('aoi_features', 'mask_id', clip_item.id) + clip_mask = ('sample_frame_features', 'sample_frame_id', clip_item.id) attributes = {} attributes['sample_frame_id'] = self.sample_frame.id diff --git a/src/view/frm_scratch_vector.py b/src/view/frm_scratch_vector.py index 1cc7787..89dce61 100644 --- a/src/view/frm_scratch_vector.py +++ b/src/view/frm_scratch_vector.py @@ -12,7 +12,6 @@ from ..model.scratch_vector import ScratchVector, insert_scratch_vector, scratch_gpkg_path, get_unique_scratch_fc_name from ..model.db_item import DBItemModel, DBItem from ..model.project import Project -from ..model.mask import AOI_MASK_TYPE_ID from ..gp.feature_class_functions import layer_path_parser from ..gp.import_feature_class import ImportFeatureClass @@ -55,10 +54,10 @@ def __init__(self, parent, iface, project: Project, import_source_path: str, vec self.txtName.setText(self.layer_name) # Masks (filtered to just AOI) - self.masks = {id: mask for id, mask in self.project.masks.items() if mask.mask_type.id == AOI_MASK_TYPE_ID} + self.clipping_masks = {id: aoi for id, aoi in self.project.aois.items()} no_clipping = DBItem('None', 0, 'None - Retain full dataset extent') - self.masks[0] = no_clipping - self.masks_model = DBItemModel(self.masks) + self.clipping_masks[0] = no_clipping + self.masks_model = DBItemModel(self.clipping_masks) self.cboMask.setModel(self.masks_model) # Default to no mask clipping self.cboMask.setCurrentIndex(self.masks_model.getItemIndex(no_clipping)) @@ -115,7 +114,7 @@ def accept(self): clip_item = self.cboMask.currentData(QtCore.Qt.UserRole) if clip_item is not None: if clip_item.id > 0: - clip_mask = ('aoi_features', 'mask_id', clip_item.id) + clip_mask = ('sample_frame_features', 'sample_frame_id', clip_item.id) if isinstance(self.import_source_path, QgsVectorLayer): task = ImportTemporaryLayer(self.import_source_path, self.txtProjectPath.text(), clip_mask=clip_mask, proj_gpkg=self.project.project_file) diff --git a/src/view/frm_valley_bottom.py b/src/view/frm_valley_bottom.py index 6fee18f..44969ef 100644 --- a/src/view/frm_valley_bottom.py +++ b/src/view/frm_valley_bottom.py @@ -6,9 +6,8 @@ from ..model.db_item import DBItem, DBItemModel from ..model.project import Project -from ..model.mask import AOI_MASK_TYPE_ID from ..model.pour_point import PourPoint -from ..model.valley_bottom import ValleyBottom, insert_valley_bottom +from ..model.sample_frame import SampleFrame, insert_sample_frame from ..model.scratch_vector import ScratchVector from ..gp.feature_class_functions import layer_path_parser @@ -21,7 +20,7 @@ class FrmValleyBottom(QtWidgets.QDialog): - def __init__(self, parent, project: Project, import_source_path: str, valley_bottom: ValleyBottom = None): + def __init__(self, parent, project: Project, import_source_path: str, valley_bottom: SampleFrame = None): self.qris_project = project self.valley_bottom = valley_bottom @@ -67,10 +66,10 @@ def __init__(self, parent, project: Project, import_source_path: str, valley_bot if show_mask_clip: # Masks (filtered to just AOI) - self.masks = {id: mask for id, mask in self.qris_project.masks.items() if mask.mask_type.id == AOI_MASK_TYPE_ID} + self.clipping_masks = {id: aoi for id, aoi in self.qris_project.aois.items()} no_clipping = DBItem('None', 0, 'None - Retain full dataset extent') - self.masks[0] = no_clipping - self.masks_model = DBItemModel(self.masks) + self.clipping_masks[0] = no_clipping + self.masks_model = DBItemModel(self.clipping_masks) self.cboMaskClip.setModel(self.masks_model) # Default to no mask clipping self.cboMaskClip.setCurrentIndex(self.masks_model.getItemIndex(no_clipping)) @@ -120,7 +119,7 @@ def accept(self): if self.valley_bottom is not None: self.valley_bottom.update(self.qris_project.project_file, self.txtName.text(), self.txtDescription.toPlainText(), metadata) else: - self.valley_bottom = insert_valley_bottom(self.qris_project.project_file, self.txtName.text(), self.txtDescription.toPlainText(), metadata) + self.valley_bottom = insert_sample_frame(self.qris_project.project_file, self.txtName.text(), self.txtDescription.toPlainText(), metadata, sample_frame_type=SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE) self.qris_project.valley_bottoms[self.valley_bottom.id] = self.valley_bottom except Exception as ex: if 'unique' in str(ex).lower(): @@ -132,16 +131,16 @@ def accept(self): if self.import_source_path is not None: try: - valley_bottom_layer_name = "valley_bottom_features" + valley_bottom_layer_name = "sample_frame_features" valley_bottom_path = f'{self.qris_project.project_file}|layername={valley_bottom_layer_name}' - layer_attributes = {'valley_bottom_id': self.valley_bottom.id} + layer_attributes = {'sample_frame_id': self.valley_bottom.id} #field_map = [ImportFieldMap(self.cboAttribute.currentData(QtCore.Qt.UserRole).name, 'display_label', direct_copy=True)] if self.cboAttribute.isVisible() else None field_map = None clip_mask = None clip_item = self.cboMaskClip.currentData(QtCore.Qt.UserRole) if clip_item is not None: if clip_item.id > 0: - clip_mask = ('aoi_features', 'mask_id', clip_item.id) + clip_mask = ('sample_frame_features', 'sample_frame_id', clip_item.id) if self.layer_id == 'memory': import_task = ImportTemporaryLayer(self.import_source_path, valley_bottom_path, layer_attributes, field_map, clip_mask, self.attribute_filter, self.qris_project.project_file) From 1fc5df9bdd2da0b79897d6e808b4f25ba0213f4a Mon Sep 17 00:00:00 2001 From: Kelly W Date: Mon, 6 Jan 2025 16:04:18 -0800 Subject: [PATCH 03/13] import aoi from temp layer bug --- src/view/frm_dockwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/frm_dockwidget.py b/src/view/frm_dockwidget.py index 0d99f7c..c34c540 100644 --- a/src/view/frm_dockwidget.py +++ b/src/view/frm_dockwidget.py @@ -1295,7 +1295,7 @@ def add_aoi(self, parent_node: QtGui.QStandardItem, mask_type_id: int, mode: int if import_source_path is None: return - frm = FrmAOI(self, self.project, import_source_path, self.project.lookup_tables['lkp_mask_types'][mask_type_id]) + frm = FrmAOI(self, self.project, import_source_path) if mode == DB_MODE_PROMOTE: db_item = parent_node.data(QtCore.Qt.UserRole) From 704f2a59c59c141a7cf9b8d05d063c1a100fc08a Mon Sep 17 00:00:00 2001 From: Kelly W Date: Tue, 7 Jan 2025 10:43:01 -0800 Subject: [PATCH 04/13] small create analysis add to map bug --- src/view/frm_dockwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/frm_dockwidget.py b/src/view/frm_dockwidget.py index c34c540..b1db399 100644 --- a/src/view/frm_dockwidget.py +++ b/src/view/frm_dockwidget.py @@ -804,7 +804,7 @@ def add_analysis(self, parent_node): frm = FrmAnalysisProperties(self, self.project) result = frm.exec_() if result is not None and result != 0: - self.add_child_to_project_tree(parent_node, frm.analysis, True) + self.add_child_to_project_tree(parent_node, frm.analysis, False) self.open_analysis(frm.analysis) def open_analysis(self, analysis: Analysis): From eda59bc645ce30e067e4dfa2f79c3fcdc73b1060 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Tue, 7 Jan 2025 14:08:15 -0800 Subject: [PATCH 05/13] Resizing Analysis Window Causes Crash #535 --- src/view/frm_dockwidget.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/view/frm_dockwidget.py b/src/view/frm_dockwidget.py index b1db399..244174b 100644 --- a/src/view/frm_dockwidget.py +++ b/src/view/frm_dockwidget.py @@ -818,11 +818,12 @@ def open_analysis(self, analysis: Analysis): if self.analysis_doc_widget is None: self.analysis_doc_widget = FrmAnalysisDocWidget(self) + self.analysis_doc_widget.configure_analysis(self.project, analysis, None) self.iface.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.analysis_doc_widget) self.analysis_doc_widget.visibilityChanged.connect(self.destroy_analysis_doc_widget) - - self.analysis_doc_widget.configure_analysis(self.project, analysis, None) - self.analysis_doc_widget.show() + else: + self.analysis_doc_widget.configure_analysis(self.project, analysis, None) + self.analysis_doc_widget.show() def open_analysis_summary(self): From 218de41be3fe3520491f208d827919e8add6858b Mon Sep 17 00:00:00 2001 From: Kelly W Date: Tue, 7 Jan 2025 14:31:30 -0800 Subject: [PATCH 06/13] Promote watershed delineation to AOI 1.0.8 #554 --- src/view/frm_mask_aoi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/frm_mask_aoi.py b/src/view/frm_mask_aoi.py index 671e395..d664318 100644 --- a/src/view/frm_mask_aoi.py +++ b/src/view/frm_mask_aoi.py @@ -146,7 +146,7 @@ def accept(self): mask_layer_name = "sample_frame_features" mask_path = f'{self.qris_project.project_file}|layername={mask_layer_name}' layer_attributes = {'sample_frame_id': self.aoi.id} - field_map = [ImportFieldMap(self.cboAttribute.currentData(QtCore.Qt.UserRole).name, 'display_label', direct_copy=True)] if self.cboAttribute.isVisible() else None + field_map = [ImportFieldMap(self.cboAttribute.currentData(QtCore.Qt.UserRole).name, 'display_label', direct_copy=True)] if self.cboAttribute.currentData(QtCore.Qt.UserRole) is not None else None clip_mask = None clip_item = self.cboMaskClip.currentData(QtCore.Qt.UserRole) if clip_item is not None: From 44ad16b4feb53f265f4044f5f6eda0f8e34aab52 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Tue, 7 Jan 2025 16:00:59 -0800 Subject: [PATCH 07/13] Clean up Stream Gages Data #553 --- src/view/frm_stream_gage_docwidget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/view/frm_stream_gage_docwidget.py b/src/view/frm_stream_gage_docwidget.py index e07f6da..8a25c2a 100644 --- a/src/view/frm_stream_gage_docwidget.py +++ b/src/view/frm_stream_gage_docwidget.py @@ -159,6 +159,8 @@ def load_discharge_plot(self): disch = [item[1] for item in data] # remove empty string values disch = [item if item != '' else None for item in disch] + # Remove non float values + disch = [item if item is None or isinstance(item, (int, float)) else None for item in disch] self._static_ax.plot(dates, disch, ".") self._static_ax.set_ylabel('Discharge (CFS)') self._static_ax.set_xlabel('Date') From 9baeead91949d126c8bfe6e1ae61efd662ef521e Mon Sep 17 00:00:00 2001 From: Kelly W Date: Wed, 8 Jan 2025 11:49:43 -0800 Subject: [PATCH 08/13] export project log message bug --- src/view/frm_export_project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/frm_export_project.py b/src/view/frm_export_project.py index cc312b3..18f6826 100644 --- a/src/view/frm_export_project.py +++ b/src/view/frm_export_project.py @@ -7,7 +7,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import QMessageBox -from qgis.core import QgsVectorLayer, QgsMessageLog +from qgis.core import Qgis, QgsVectorLayer, QgsMessageLog from qgis.utils import iface from qgis.PyQt.QtCore import QSettings @@ -322,7 +322,7 @@ def accept(self) -> None: for f in lyr.getFeatures(): feature_geom = f.geometry() if feature_geom.isNull() or feature_geom.isEmpty(): - QgsMessageLog.logMessage(f"Feature {f.id()} in layer {layer} has no geometry", 'QRiS', QgsMessageLog.WARNING) + QgsMessageLog.logMessage(f"Feature {f.id()} in layer {layer} has no geometry", 'QRiS', Qgis.Warning) continue if geom is None: geom = f.geometry() @@ -354,7 +354,7 @@ def accept(self) -> None: for f in lyr.getFeatures(): feature_geom = f.geometry() if feature_geom.isNull() or feature_geom.isEmpty(): - QgsMessageLog.logMessage(f"Feature {f.id()} in layer {layer} has no geometry", 'QRiS', QgsMessageLog.WARNING) + QgsMessageLog.logMessage(f"Feature {f.id()} in layer {layer} has no geometry", 'QRiS', Qgis.Warning) continue if geom is None: geom = feature_geom From 8458808c00384ddc2ee773413d258fe9af7db2ca Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 10 Jan 2025 14:36:51 -0800 Subject: [PATCH 09/13] update duplicate name validation for sample frames --- src/view/frm_sample_frame.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/view/frm_sample_frame.py b/src/view/frm_sample_frame.py index 60a0cc8..d567c58 100644 --- a/src/view/frm_sample_frame.py +++ b/src/view/frm_sample_frame.py @@ -1,5 +1,6 @@ import os import json +import sqlite3 from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtCore import Qt, QSize, QVariant, pyqtSignal @@ -120,8 +121,11 @@ def accept(self): return # validate name is unique - if self.sample_frame is None: - if not validate_name_unique(self.qris_project.project_file, 'sample_frames', 'name', self.txtName.text()): + with sqlite3.connect(self.qris_project.project_file) as conn: + curs = conn.cursor() + curs.execute('SELECT name FROM sample_frames WHERE name = ? and sample_frame_type_id = 1 and not id = ?', [self.txtName.text(), self.sample_frame.id if self.sample_frame is not None else 0]) + row = curs.fetchone() + if row is not None: QMessageBox.warning(self, 'Duplicate Name', f"A sample frame with the name '{self.txtName.text()}' already exists. Please choose a unique name.") return From d12ff2221b3928895756d5a88e1ee334293530c1 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 10 Jan 2025 14:37:35 -0800 Subject: [PATCH 10/13] drop unique constraint on sampe frame names between different sample frame types --- src/db/migrations/028_masks.sql | 40 +++++++++++++++++++++++++++++---- src/model/analysis.py | 4 ++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/db/migrations/028_masks.sql b/src/db/migrations/028_masks.sql index 650fcb9..c51dc3e 100644 --- a/src/db/migrations/028_masks.sql +++ b/src/db/migrations/028_masks.sql @@ -1,4 +1,5 @@ -- sqlite +PRAGMA foreign_keys=OFF; -- Create new sample frame types table CREATE TABLE sample_frame_types ( @@ -14,11 +15,40 @@ INSERT INTO sample_frame_types (id, name, description, metadata) VALUES (1, 'sam INSERT INTO sample_frame_types (id, name, description, metadata) VALUES (2, 'aoi', 'Area of Interest', '{}'); INSERT INTO sample_frame_types (id, name, description, metadata) VALUES (3, 'valley_bottom', 'Valley Bottom', '{}'); --- Add sample_frame_type_id to sample frames that references the sample_frame_types table -ALTER TABLE sample_frames ADD COLUMN sample_frame_type_id INTEGER REFERENCES sample_frame_types(id); +-- Recreate the sample frames table without unique constraint on name +CREATE TABLE sample_frames_temp AS SELECT * FROM sample_frames; +CREATE TABLE anaysis_temp AS SELECT * FROM analyses; --- the current sample frames are all sample_frame type -UPDATE sample_frames SET sample_frame_type_id = 1; +DROP TABLE sample_frames; +DROP TABLE analyses; + +CREATE TABLE sample_frames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + sample_frame_type_id INTEGER NOT NULL REFERENCES sample_frame_types(id), + description TEXT, + metadata TEXT, + created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE INDEX idx_sample_frames_name ON sample_frames (name, sample_frame_type_id); + +INSERT INTO sample_frames (id, name, sample_frame_type_id, description, metadata, created_on) + SELECT id, name, 1, description, metadata, created_on FROM sample_frames_temp; +DROP TABLE sample_frames_temp; + +CREATE TABLE analyses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + sample_frame_id INTEGER NOT NULL REFERENCES "sample_frames"(id), + description TEXT, + metadata TEXT, + created_on DATETIME DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO analyses (id, name, sample_frame_id, description, metadata, created_on) + SELECT id, name, mask_id, description, metadata, created_on FROM anaysis_temp; +DROP TABLE anaysis_temp; -- copy the aois from the masks table to the sample_frames table INSERT INTO sample_frames (name, description, metadata, sample_frame_type_id) @@ -56,3 +86,5 @@ DROP TABLE masks; -- remove the entires for gpkg_contents and gpkg_geometry_columns DELETE FROM gpkg_geometry_columns WHERE table_name IN ('aoi_features', 'mask_features', 'valley_bottom_features'); DELETE FROM gpkg_contents WHERE table_name IN ('aoi_features', 'masks', 'valley_bottom_features', 'valley_bottoms', 'mask_features'); + +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/src/model/analysis.py b/src/model/analysis.py index b4729c7..fcdb2e4 100644 --- a/src/model/analysis.py +++ b/src/model/analysis.py @@ -54,7 +54,7 @@ def load_analyses(curs: sqlite3.Cursor, sample_frames: dict, metrics: dict) -> d row['id'], row['name'], row['description'], - sample_frames[row['mask_id']], + sample_frames[row['sample_frame_id']], json.loads(row['metadata']) if row['metadata'] is not None else None ) for row in curs.fetchall()} @@ -76,7 +76,7 @@ def insert_analysis(db_path: str, name: str, description: str, sample_frame: Sam with sqlite3.connect(db_path) as conn: try: curs = conn.cursor() - curs.execute('INSERT INTO analyses (name, description, mask_id, metadata) VALUES (?, ?, ?, ?)', [ + curs.execute('INSERT INTO analyses (name, description, sample_frame_id, metadata) VALUES (?, ?, ?, ?)', [ name, description if description is not None and len(description) > 0 else None, sample_frame.id, metadata_str]) analysis_id = curs.lastrowid analysis = Analysis(analysis_id, name, description, sample_frame, metadata=metadata) From 2dbe0efb7278bf637c5e4142fad8b175aa8f1de1 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Tue, 14 Jan 2025 12:30:48 -0800 Subject: [PATCH 11/13] Add Clip Option when importing valley bottom features from temporary layer #555 --- src/view/frm_valley_bottom.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/view/frm_valley_bottom.py b/src/view/frm_valley_bottom.py index 44969ef..027e617 100644 --- a/src/view/frm_valley_bottom.py +++ b/src/view/frm_valley_bottom.py @@ -47,10 +47,7 @@ def __init__(self, parent, project: Project, import_source_path: str, valley_bot if isinstance(import_source_path, QgsVectorLayer): self.layer_name = import_source_path.name() self.layer_id = 'memory' - show_mask_clip = False - self.cboMaskClip.setVisible(False) - self.lblMaskClip.setVisible(False) - else: + show_mask_clip = True # find if import_source_path is shapefile, geopackage, or other self.basepath, self.layer_name, self.layer_id = layer_path_parser(import_source_path) From 78f563fe20c4439408c2ebc788bd2d47d12cf35b Mon Sep 17 00:00:00 2001 From: Kelly W Date: Wed, 15 Jan 2025 13:54:20 -0800 Subject: [PATCH 12/13] clean up migration code and do not recreate legacy layers --- src/model/project.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/model/project.py b/src/model/project.py index 7cd6537..67129ed 100644 --- a/src/model/project.py +++ b/src/model/project.py @@ -47,6 +47,12 @@ ('dce_polygons', 'DCE Polygons', 'Polygon') ] +# migrated layers not to be recreated +migrated_layers = [ + 'aoi_features', + 'mask_features', + 'valley_bottom_features' +] class Project(DBItem): @@ -227,6 +233,7 @@ def apply_db_migrations(db_path: str): curs = conn.cursor() existing_layers = [layer[0] for layer in curs.execute('SELECT table_name, data_type FROM gpkg_contents WHERE data_type = "features"').fetchall()] + existing_layers = existing_layers + migrated_layers for fc_name, layer_name, geometry_type in project_layers: if fc_name not in existing_layers: @@ -244,16 +251,16 @@ def apply_db_migrations(db_path: str): if migration_row is None: try: migration_path = os.path.join(migrations_dir, migration_file) + QgsMessageLog.logMessage(f'Appling QRiS Database Migrations: {migration_file}', 'QRiS', Qgis.Info) with open(migration_path, 'r') as f: sql_commands = f.read() - curs.executescript(sql_commands) + conn.execute('BEGIN') + curs.executescript(sql_commands) curs.execute('INSERT INTO migrations (file_name) VALUES (?)', [migration_file]) - QgsMessageLog.logMessage(f'Appling QRiS Database Migrations: {migration_file}', 'QRiS', Qgis.Info) + conn.commit() except Exception as ex: conn.rollback() raise ex - - conn.commit() except Exception as ex: conn.rollback() raise ex From 2bb3e1fd7c52be7cb2554de3a973daf032c0c208 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Wed, 15 Jan 2025 13:59:23 -0800 Subject: [PATCH 13/13] version 1.0.8 --- CHANGELIST.md | 17 +++++++++++++++++ __version__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELIST.md b/CHANGELIST.md index 98c1753..7b75575 100644 --- a/CHANGELIST.md +++ b/CHANGELIST.md @@ -1,5 +1,22 @@ # QRiS Plugin +## [1.0.8] 2025 JAN 14 + +### Added +- Add Clip Option when importing valley bottom features from temporary layer #555 + +### Fixed +- Resizing Analysis Window Causes Crash #535 +- Promote watershed delineation to AOI bug #554 +- Bug with message log when exporting project #558 +- Bug with plotting non-numeric values in stream gage data #553 + +### Changed +- AOIs and VBs now stored in db as Sample Frames to support future metric capability #550 +- Clean up migration code so it rolls back properly if an error occurs +- Do not recreate legacy layers when opening an existing project + + ## [1.0.7] 2024 DEC 13 ### Fixed diff --git a/__version__.py b/__version__.py index 84ce141..a741792 100644 --- a/__version__.py +++ b/__version__.py @@ -1 +1 @@ -__version__ = "1.0.7" \ No newline at end of file +__version__ = "1.0.8" \ No newline at end of file