Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 1.0.9 #565

Merged
merged 9 commits into from
Jan 24, 2025
17 changes: 17 additions & 0 deletions CHANGELIST.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# QRiS Plugin

## [1.0.9] 2025 JAN 24

### Added
- Allow clip to AOI when importing existing feature class into DCE layer #562

### Fixed
- Bugs with AOI properties form (unique name constraints)
- Promote to Valley Bottom Icon
- Fix Virtual Area Fields and VB,AOI field fixes #564
- Mapping multiple fields in an existing feature class to DCE Layer causes error #552

### Changed
- Analysis allow for metrics to be calculated for AOI's and Valley Bottoms #561
- Add AOI's and Valley Bottoms to Climate Engine #561
- Import Field Mapping Form improvements #552


## [1.0.8] 2025 JAN 14

### Added
Expand Down
2 changes: 1 addition & 1 deletion __version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.8"
__version__ = "1.0.9"
8 changes: 7 additions & 1 deletion src/QRiS/qris_map_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ def build_aoi_layer(self, aoi: SampleFrame) -> QgsMapLayer:
# setup fields
self.set_hidden(feature_layer, 'fid', 'AOI Feature ID')
self.set_hidden(feature_layer, 'sample_frame_id', 'AOI ID')
self.set_alias(feature_layer, 'display_label', 'Display Label')
self.set_alias(feature_layer, 'position', 'Position')
self.set_multiline(feature_layer, 'description', 'Description')
self.set_hidden(feature_layer, 'metadata', 'Metadata')
self.set_hidden(feature_layer, 'flow_path', 'Flow Path', hide_in_attribute_table=True)
self.set_hidden(feature_layer, 'flows_into', 'Flows Into', hide_in_attribute_table=True)
self.set_virtual_dimension(feature_layer, 'area')
self.set_metadata_virtual_fields(feature_layer)

Expand Down Expand Up @@ -172,8 +175,11 @@ def build_valley_bottom_layer(self, valley_bottom: SampleFrame) -> QgsMapLayer:
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, 'fid', 'VB Feature ID')
self.set_hidden(feature_layer, 'sample_frame_id', 'Valley Bottom ID')
self.set_alias(feature_layer, 'display_label', 'Display Label')
self.set_hidden(feature_layer, 'flow_path', 'Flow Path', hide_in_attribute_table=True)
self.set_hidden(feature_layer, 'flows_into', 'Flows Into', hide_in_attribute_table=True)
self.set_multiline(feature_layer, 'description', 'Description')
self.set_hidden(feature_layer, 'metadata', 'Metadata')
self.set_virtual_dimension(feature_layer, 'area')
Expand Down
103 changes: 32 additions & 71 deletions src/QRiS/riverscapes_map_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,71 +518,6 @@ def set_metadata_virtual_fields(self, feature_layer: QgsVectorLayer, field_confi
widget = QgsEditorWidgetSetup('Hidden', {})
feature_layer.setEditorWidgetSetup(feature_layer.fields().indexFromName(key), widget)

# if field_type == QVariant.Url:
# set the widget to open the url

# add an action edit the value
# edit_action_text = dedent("""
# from qgis.PyQt.QtWidgets import QLineEdit, QDialog, QDialogButtonBox, QVBoxLayout
# from qgis.PyQt.QtCore import Qt
# from qgis.PyQt.QtGui import QIntValidator, QDoubleValidator
# from qgis.core import QgsExpressionContextUtils

# class EditMetadata(QDialog):
# def __init__(self, parent=None):

# QDialog.__init__(self, parent)
# self.setWindowTitle('Edit Metadata')
# self.setWindowFlags(Qt.WindowStaysOnTopHint)
# self.setLayout(QVBoxLayout())
# self.layout().setContentsMargins(0, 0, 0, 0)
# self.layout().setSpacing(0)
# self.layout().setAlignment(Qt.AlignTop)
# self.layout().setAlignment(Qt.AlignLeft)

# self.metadata = QgsExpressionContextUtils.layerScope(iface.activeLayer()).variable('metadata')
# self.

# self.metadata_edit = QLineEdit()
# self.metadata_edit.setText(self.metadata)
# self.layout().addWidget(self.metadata_edit)

# self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
# self.button_box.accepted.connect(self.accept)
# self.button_box.rejected.connect(self.reject)
# self.layout().addWidget(self.button_box)

# def accept(self):
# self.metadata = self.metadata_edit.text()
# QgsExpressionContextUtils.setLayerVariable(iface.activeLayer(), 'metadata', self.metadata)
# self.super().accept()

# def reject(self):
# self.super().reject()

# dialog = EditMetadata()
# dialog.exec_()
# """).strip("\n")

# editAction = QgsAction(1, 'Edit Metadata', edit_action_text, None, capture=False, shortTitle='Edit Metadata', actionScopes={'Feature'})
# feature_layer.actions().addAction(editAction)
# editorAction = QgsAttributeEditorAction(editAction)

# feature_layer.setEditorWidgetSetup(feature_layer.fields().indexFromName(key), QgsEditorWidgetSetup('TextEdit', {'IsMultiline': True, 'UseHtml': False, 'Action': editorAction}))

# if field_type == QVariant.Url:
# # add an action to open the url
# url_action_text = dedent("""
# import webbrowser
# url = "[% @url %]"
# webbrowser.open(url, new=2)
# """).strip("\n")

# urlAction = QgsAction(1, 'Open URL', url_action_text, None, capture=False, shortTitle='Open URL', actionScopes={'Feature'})
# feature_layer.actions().addAction(urlAction)
# editorAction = QgsAttributeEditorAction(urlAction)
# feature_layer.setEditorWidgetSetup(feature_layer.fields().indexFromName(key), QgsEditorWidgetSetup('OpenUrl', {'Action': editorAction}))

# set the default value for the metadata field
feature_layer.setDefaultValueDefinition(field_index, QgsDefaultValue('\'{}\''))

Expand All @@ -596,7 +531,7 @@ def set_multiline(self, feature_layer: QgsVectorLayer, field_name: str, field_al
form_config.setLabelOnTop(field_index, True)
feature_layer.setEditFormConfig(form_config)

def set_hidden(self, feature_layer: QgsVectorLayer, field_name: str, field_alias: str) -> None:
def set_hidden(self, feature_layer: QgsVectorLayer, field_name: str, field_alias: str, hide_in_attribute_table=False) -> None:
"""Sets a field to hidden, read only, and also sets an alias just in case. Often used on fid, project_id, and event_id"""
fields = feature_layer.fields()
field_index = fields.indexFromName(field_name)
Expand All @@ -607,6 +542,16 @@ def set_hidden(self, feature_layer: QgsVectorLayer, field_name: str, field_alias
widget_setup = QgsEditorWidgetSetup('Hidden', {})
feature_layer.setEditorWidgetSetup(field_index, widget_setup)

if hide_in_attribute_table:
config = feature_layer.attributeTableConfig()
columns = config.columns()
for column in columns:
if column.name == field_name:
column.hidden = True
break
config.setColumns(columns)
feature_layer.setAttributeTableConfig(config)

def set_alias(self, feature_layer: QgsVectorLayer, field_name: str, field_alias: str, parent_container=None, display_index=None) -> None:
"""Just provides an alias to the field for display"""
fields = feature_layer.fields()
Expand Down Expand Up @@ -689,15 +634,31 @@ def set_virtual_dimension(self, feature_layer: QgsVectorLayer, dimension: str) -
sets the widget type to text
sets default value to the dimension expression"""

field_name = 'vrt_' + dimension
field_alias = dimension.capitalize() + ' (m)'
field_expression = 'round(${}, 0)'.format(dimension)
virtual_field = QgsField(field_name, QVariant.Int)
if dimension == 'area':
field_name = 'vrt_area'
field_alias = 'Area (m²)'
if feature_layer.crs().isGeographic():
# Use transform function to reproject geometry to EPSG:5070 for area calculation
field_expression = 'round(area(transform($geometry, \'EPSG:4326\', \'EPSG:5070\')), 0)'
else:
field_expression = 'round($area, 0)'
elif dimension == 'length':
field_name = 'vrt_length'
field_alias = 'Length (m)'
if feature_layer.crs().isGeographic():
# Use transform function to reproject geometry to EPSG:5070 for length calculation
field_expression = 'round(length(transform($geometry, \'EPSG:4326\', \'EPSG:5070\')), 0)'
else:
field_expression = 'round($length, 0)'
else:
raise ValueError("Dimension must be 'area' or 'length'")

virtual_field = QgsField(field_name, QVariant.Double)
feature_layer.addExpressionField(field_expression, virtual_field)
fields = feature_layer.fields()
field_index = fields.indexFromName(field_name)
feature_layer.setFieldAlias(field_index, field_alias)
feature_layer.setDefaultValueDefinition(field_index, QgsDefaultValue(field_expression))
feature_layer.setDefaultValueDefinition(field_index, QgsDefaultValue(field_expression, True))
widget_setup = QgsEditorWidgetSetup('TextEdit', {})
feature_layer.setEditorWidgetSetup(field_index, widget_setup)

Expand Down
8 changes: 7 additions & 1 deletion src/model/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,21 @@ def __init__(self, project_file: str):
self.events = load_events(curs, self.protocols, self.methods, self.layers, self.lookup_tables, self.rasters)
self.planning_containers = load_planning_containers(curs, self.events)
self.metrics = load_metrics(curs)
self.analyses = load_analyses(curs, self.sample_frames, self.metrics)
self.pour_points = load_pour_points(curs)
self.stream_gages = load_stream_gages(curs)
self.profiles = load_profiles(curs)
self.cross_sections = load_cross_sections(curs)
self.valley_bottoms = load_sample_frames(curs, sample_frame_type=SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE)
self.analyses = load_analyses(curs, self.analysis_masks(), self.metrics)

self.units = load_units(curs)

def analysis_masks(self) -> dict:
masks = self.sample_frames.copy()
masks.update(self.aois)
masks.update(self.valley_bottoms)
return masks

def get_relative_path(self, absolute_path: str) -> str:
return parse_posix_path(os.path.relpath(absolute_path, os.path.dirname(self.project_file)))

Expand Down
2 changes: 1 addition & 1 deletion src/view/frm_analysis_docwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def setupUi(self):
self.cmdCalculate.setToolTipDuration(2000)
self.horizEvent.addWidget(self.cmdCalculate, 0)

self.lblSegment = QtWidgets.QLabel('Sample Frame Label')
self.lblSegment = QtWidgets.QLabel('Mask Polygon')
self.grid.addWidget(self.lblSegment, 2, 0, 1, 1)

self.cboSampleFrame = QtWidgets.QComboBox()
Expand Down
20 changes: 15 additions & 5 deletions src/view/frm_analysis_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ def __init__(self, parent, project: Project, analysis: Analysis = None):
self.setupUi()

# Sample Frames
self.sampling_frames = {id: sample_frame for id, sample_frame in project.sample_frames.items()}
self.sampling_frames = {id: sample_frame for id, sample_frame in project.analysis_masks().items()}
self.sampling_frames_model = DBItemModel(self.sampling_frames)
self.cboSampleFrame.setModel(self.sampling_frames_model)
self.cboSampleFrame.currentIndexChanged.connect(self.on_cboSampleFrame_currentIndexChanged)

# Valley Bottoms
self.valley_bottoms = {id: valley_bottom for id, valley_bottom in project.valley_bottoms.items()}
Expand Down Expand Up @@ -142,6 +143,18 @@ def toggle_all_metrics(self, level_id: str):
idx = cboStatus.findText(level_id)
cboStatus.setCurrentIndex(idx)

def on_cboSampleFrame_currentIndexChanged(self, index):

# if the sample frame type is Valley Bottom, then set the Valley Bottom combo box to the selected valley bottom as well, then lock that combo box. if not, then unlock the combo box
sample_frame: SampleFrame = self.cboSampleFrame.currentData(QtCore.Qt.UserRole)
if sample_frame is not None:
if sample_frame.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE:
index = self.cboValleyBottom.findData(sample_frame)
self.cboValleyBottom.setCurrentIndex(index)
self.cboValleyBottom.setEnabled(False)
else:
self.cboValleyBottom.setEnabled(True)

def setupUi(self):

self.setMinimumSize(500, 500)
Expand All @@ -159,15 +172,12 @@ def setupUi(self):
self.txtName = QtWidgets.QLineEdit()
self.grdLayout1.addWidget(self.txtName, 0, 1, 1, 1)

self.lblSampleFrame = QtWidgets.QLabel('Sample Frame')
self.lblSampleFrame = QtWidgets.QLabel('Analysis Masks (Sample Frame)')
self.grdLayout1.addWidget(self.lblSampleFrame, 1, 0, 1, 1)

self.cboSampleFrame = QtWidgets.QComboBox()
self.grdLayout1.addWidget(self.cboSampleFrame, 1, 1, 1, 1)

# self.groupboxInputs = QtWidgets.QGroupBox('Inputs')
# self.vert.addWidget(self.groupboxInputs)

self.lblValleyBottom = QtWidgets.QLabel('Valley Bottom')
self.grdLayout1.addWidget(self.lblValleyBottom, 2, 0, 1, 1)

Expand Down
3 changes: 2 additions & 1 deletion src/view/frm_climate_engine_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .utilities import add_help_button

from ..model.project import Project
from ..model.sample_frame import SampleFrame
from ..lib.climate_engine import get_datasets, get_dataset_variables, get_dataset_date_range, get_dataset_timeseries_polygon, open_climate_engine_website


Expand All @@ -27,7 +28,7 @@ def __init__(self, parent: QtWidgets.QWidget = None, qris_project: Project = Non
self.iface = iface
self.layer_geometry = None
self.qris_project = qris_project
self.sample_frame_widget = SampleFrameWidget(self, qris_project)
self.sample_frame_widget = SampleFrameWidget(self, qris_project, sample_frame_types=[SampleFrame.AOI_SAMPLE_FRAME_TYPE, SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE, SampleFrame.SAMPLE_FRAME_TYPE])
self.date_range_widget = DateRangeWidget(self, horizontal=False)

self.setWindowTitle('Download Climate Engine Metrics')
Expand Down
4 changes: 3 additions & 1 deletion src/view/frm_climate_engine_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

from ..model.project import Project
from ..model.db_item import dict_factory
from ..model.sample_frame import SampleFrame
from ..model.basin_characteristics_table_view import BasinCharsTableModel

from ..lib.climate_engine import get_datasets, open_climate_engine_website
from ..QRiS.qris_map_manager import QRisMapManager

Expand All @@ -34,7 +36,7 @@ def __init__(self, parent: QtWidgets.QWidget, qris_project: Project, qris_map_ma
self.qris_map_manager = qris_map_manager
self.datasets = get_datasets()

self.sample_frame_widget = SampleFrameWidget(self, self.qris_project, self.qris_map_manager, first_index_empty=True)
self.sample_frame_widget = SampleFrameWidget(self, self.qris_project, self.qris_map_manager, first_index_empty=True, sample_frame_types=[SampleFrame.AOI_SAMPLE_FRAME_TYPE, SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE, SampleFrame.SAMPLE_FRAME_TYPE])
self.sample_frame_widget.cbo_sample_frame.currentIndexChanged.connect(self.load_climate_engine_metrics)
self.sample_frame_widget.sample_frame_changed.connect(self.load_climate_engine_metrics)
self.sample_frame_widget.sample_frame_changed.connect(self.create_plot)
Expand Down
4 changes: 2 additions & 2 deletions src/view/frm_dockwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,8 @@ def open_menu(self, position):
# 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, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_PROMOTE))
self.add_context_menu_item(promote_menu, 'Riverscape Valley Bottom', 'valley_bottom', lambda: self.add_valley_bottom(model_item, 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:
promote_menu = self.menu.addMenu('Promote to ...')
self.add_context_menu_item(promote_menu, 'Profile', 'gis', lambda: self.add_profile(model_item, DB_MODE_PROMOTE))
Expand Down Expand Up @@ -1559,7 +1559,7 @@ def edit_item(self, model_item: QtGui.QStandardItem, db_item: DBItem):
frm = FrmEvent(self, self.project, event=db_item, event_type_id=db_item.event_type.id)
elif isinstance(db_item, SampleFrame):
if db_item.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE:
frm = FrmAOI(self, self.project, None, db_item.sample_frame_type, db_item)
frm = FrmAOI(self, self.project, None, db_item)
elif db_item.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE:
frm = FrmValleyBottom(self, self.project, None, db_item)
else:
Expand Down
Loading