Skip to content

Commit

Permalink
Output related changes:
Browse files Browse the repository at this point in the history
* Store unrounded window in JSON
* Rename cellsize -> spacing consistently
* output_options: dict to typed NamedTuple
* store spacing in output_options
* Rename convert to Python button to Save as Python
* Create Save as JSON button
* Move buttons to top, in GeoPackage Group
  • Loading branch information
Huite committed Oct 29, 2023
1 parent 8377509 commit 4f37f78
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 67 deletions.
46 changes: 23 additions & 23 deletions plugin/qgistim/core/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ def round_spacing(ymin: float, ymax: float) -> float:
return dy


def round_extent(domain: Dict[str, float], cellsize: float) -> Tuple[float]:
def round_extent(domain: Dict[str, float], spacing: float) -> Tuple[float]:
"""
Increases the extent until all sides lie on a coordinate
divisible by cellsize.
divisible by spacing.
Parameters
----------
extent: Tuple[float]
xmin, xmax, ymin, ymax
cellsize: float
spacing: float
Desired cell size of the output head grids
Returns
Expand All @@ -96,25 +96,25 @@ def round_extent(domain: Dict[str, float], cellsize: float) -> Tuple[float]:
ymin = domain["ymin"]
xmax = domain["xmax"]
ymax = domain["ymax"]
xmin = np.floor(xmin / cellsize) * cellsize
ymin = np.floor(ymin / cellsize) * cellsize
xmax = np.ceil(xmax / cellsize) * cellsize
ymax = np.ceil(ymax / cellsize) * cellsize
xmin += 0.5 * cellsize
xmax += 0.5 * cellsize
ymax -= 0.5 * cellsize
xmin -= 0.5 * cellsize
xmin = np.floor(xmin / spacing) * spacing
ymin = np.floor(ymin / spacing) * spacing
xmax = np.ceil(xmax / spacing) * spacing
ymax = np.ceil(ymax / spacing) * spacing
xmin += 0.5 * spacing
xmax += 0.5 * spacing
ymax -= 0.5 * spacing
xmin -= 0.5 * spacing
return xmin, xmax, ymin, ymax


def headgrid_entry(domain: Dict[str, float], cellsize: float) -> Dict[str, float]:
(xmin, xmax, ymin, ymax) = round_extent(domain, cellsize)
def headgrid_entry(domain: Dict[str, float], spacing: float) -> Dict[str, float]:
(xmin, xmax, ymin, ymax) = round_extent(domain, spacing)
return {
"xmin": xmin,
"xmax": xmax,
"ymin": ymin,
"ymax": ymax,
"spacing": cellsize,
"spacing": spacing,
"time": domain.get("time"),
}

Expand Down Expand Up @@ -259,7 +259,6 @@ def json_elements_and_observations(data, mapping: Dict[str, str]):

def timml_json(
timml_data: Dict[str, Any],
cellsize: float,
output_options: Dict[str, bool],
) -> Dict[str, Any]:
"""
Expand All @@ -285,40 +284,41 @@ def timml_json(
)
json_data = {
"timml": timml_json,
"output_options": output_options,
"headgrid": headgrid_entry(domain_data, cellsize),
"observations": observations,
"window": domain_data,
}
if output_options:
json_data["output_options"] = output_options._asdict()
json_data["headgrid"] = headgrid_entry(domain_data, output_options.spacing)
return json_data


def ttim_json(
timml_data: Dict[str, Any],
ttim_data: Dict[str, Any],
cellsize: float,
output_options: Dict[str, bool],
) -> Dict[str, Any]:
json_data = timml_json(timml_data, cellsize, output_options)
json_data = timml_json(timml_data, output_options)

data = ttim_data.copy()
domain_data = data.pop("timml Domain:Domain")
start_date = data.pop("start_date")
ttim_json, observations = json_elements_and_observations(data, mapping=TTIM_MAPPING)

json_data["ttim"] = ttim_json
json_data["headgrid"] = headgrid_entry(domain_data, cellsize)
json_data["start_date"] = start_date
json_data["observations"] = observations
if output_options:
json_data["headgrid"] = headgrid_entry(domain_data, output_options.spacing)
return json_data


def data_to_json(
timml_data: Dict[str, Any],
ttim_data: Union[Dict[str, Any], None],
cellsize: float,
output_options: Dict[str, bool],
) -> Dict[str, Any]:
if ttim_data is None:
return timml_json(timml_data, cellsize, output_options)
return timml_json(timml_data, output_options)
else:
return ttim_json(timml_data, ttim_data, cellsize, output_options)
return ttim_json(timml_data, ttim_data, output_options)
69 changes: 40 additions & 29 deletions plugin/qgistim/widgets/compute_widget.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from pathlib import Path
from typing import Tuple, Union
from typing import NamedTuple, Tuple, Union

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
Expand Down Expand Up @@ -32,6 +32,15 @@
from qgistim.core.task import BaseServerTask


class OutputOptions(NamedTuple):
raster: bool
mesh: bool
contours: bool
head_observations: bool
discharge: bool
spacing: float


class ComputeTask(BaseServerTask):
@property
def task_description(self):
Expand All @@ -55,12 +64,14 @@ def finished(self, result):
if result:
self.push_success_message()
self.parent.clear_outdated_output(self.data["path"])
if self.data["head_observations"] or self.data["discharge"]:
self.parent.load_vector_result(self.data["path"])
if self.data["mesh"]:
self.parent.load_mesh_result(self.data["path"], self.data["contours"])
if self.data["raster"]:
self.parent.load_raster_result(self.data["path"])
path = self.data["path"]
output = self.data["output_options"]
if output.head_observations or output.discharge:
self.parent.load_vector_result(path)
if output.mesh:
self.parent.load_mesh_result(path, output.contours)
if output.raster:
self.parent.load_raster_result(path)

else:
self.push_failure_message()
Expand All @@ -85,10 +96,10 @@ def __init__(self, parent=None):
self.discharge_checkbox = QCheckBox("Discharge")
self.discharge_observations_checkbox = QCheckBox("Discharge Observations")

self.cellsize_spin_box = QDoubleSpinBox()
self.cellsize_spin_box.setMinimum(0.0)
self.cellsize_spin_box.setMaximum(10_000.0)
self.cellsize_spin_box.setSingleStep(1.0)
self.spacing_spin_box = QDoubleSpinBox()
self.spacing_spin_box.setMinimum(0.0)
self.spacing_spin_box.setMaximum(10_000.0)
self.spacing_spin_box.setSingleStep(1.0)
self.domain_button.clicked.connect(self.domain)
# By default: all output
self.mesh_checkbox.toggled.connect(self.contours_checkbox.setEnabled)
Expand Down Expand Up @@ -131,7 +142,7 @@ def __init__(self, parent=None):

domain_row = QHBoxLayout()
domain_row.addWidget(QLabel("Grid spacing"))
domain_row.addWidget(self.cellsize_spin_box)
domain_row.addWidget(self.spacing_spin_box)
domain_layout.addWidget(self.domain_button)
domain_layout.addLayout(domain_row)

Expand Down Expand Up @@ -174,7 +185,7 @@ def __init__(self, parent=None):
self.setLayout(layout)

def reset(self):
self.cellsize_spin_box.setValue(25.0)
self.spacing_spin_box.setValue(25.0)
self.output_line_edit.setText("")
self.mesh_checkbox.setChecked(True)
self.raster_checkbox.setChecked(False)
Expand Down Expand Up @@ -298,29 +309,29 @@ def compute(self) -> None:
Run a TimML computation with the current state of the currently active
GeoPackage dataset.
"""
cellsize = self.cellsize_spin_box.value()
transient = self.parent.dataset_widget.transient
output_options = {
"raster": self.raster_checkbox.isChecked(),
"mesh": self.mesh_checkbox.isChecked(),
"contours": self.contours_checkbox.isChecked(),
"head_observations": self.head_observations_checkbox.isChecked(),
"discharge": self.discharge_checkbox.isChecked(),
}
output_options = OutputOptions(
raster=self.raster_checkbox.isChecked(),
mesh=self.mesh_checkbox.isChecked(),
contours=self.contours_checkbox.isChecked(),
head_observations=self.head_observations_checkbox.isChecked(),
discharge=self.discharge_checkbox.isChecked(),
spacing=self.spacing_spin_box.value(),
)

path = Path(self.output_path).absolute().with_suffix(".json")
invalid_input = self.parent.dataset_widget.convert_to_json(
path, cellsize=cellsize, transient=transient, output_options=output_options
path, transient=transient, output_options=output_options
)
# Early return in case some problems are found.
if invalid_input:
return

data = {
task_data = {
"operation": "compute",
"path": str(path),
"transient": transient,
**output_options,
"output_options": output_options,
}
# https://gis.stackexchange.com/questions/296175/issues-with-qgstask-and-task-manager
# It seems the task goes awry when not associated with a Python object!
Expand All @@ -336,7 +347,7 @@ def compute(self) -> None:
if Path(gpkg_path) == Path(layer.source()):
QgsProject.instance().removeMapLayer(layer.id())

self.compute_task = ComputeTask(self, data, self.parent.message_bar)
self.compute_task = ComputeTask(self, task_data, self.parent.message_bar)
self.start_task = self.parent.start_interpreter_task()
if self.start_task is not None:
self.compute_task.addSubTask(
Expand All @@ -352,12 +363,12 @@ def domain(self) -> None:
"""
item = self.parent.domain_item()
ymax, ymin = item.element.update_extent(self.parent.iface)
self.set_cellsize_from_domain(ymax, ymin)
self.set_spacing_from_domain(ymax, ymin)
self.parent.iface.mapCanvas().refreshAllLayers()
return

def set_cellsize_from_domain(self, ymax: float, ymin: float) -> None:
# Guess a reasonable value for the cellsize: about 50 rows
def set_spacing_from_domain(self, ymax: float, ymin: float) -> None:
# Guess a reasonable value for the spacing: about 50 rows
dy = (ymax - ymin) / 50.0
if dy > 500.0:
dy = round(dy / 500.0) * 500.0
Expand All @@ -367,7 +378,7 @@ def set_cellsize_from_domain(self, ymax: float, ymin: float) -> None:
dy = round(dy / 5.0) * 5.0
elif dy > 1.0:
dy = round(dy)
self.cellsize_spin_box.setValue(dy)
self.spacing_spin_box.setValue(dy)
return

def load_mesh_result(self, path: Union[Path, str], load_contours: bool) -> None:
Expand Down
35 changes: 23 additions & 12 deletions plugin/qgistim/widgets/dataset_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from qgis.core import Qgis, QgsProject, QgsUnitTypes
from qgistim.core.elements import Aquifer, Domain, load_elements_from_geopackage
from qgistim.core.formatting import data_to_json, data_to_script
from qgistim.widgets.compute_widget import OutputOptions
from qgistim.widgets.error_window import ValidationDialog

SUPPORTED_TTIM_ELEMENTS = set(
Expand Down Expand Up @@ -287,8 +288,10 @@ def __init__(self, parent):
self.suppress_popup_checkbox.stateChanged.connect(self.suppress_popup_changed)
self.remove_button.clicked.connect(self.remove_geopackage_layer)
self.add_button.clicked.connect(self.add_selection_to_qgis)
self.convert_button = QPushButton("Export to Python script")
self.convert_button.clicked.connect(self.convert_to_python)
self.python_convert_button = QPushButton("Save as Python")
self.python_convert_button.clicked.connect(self.save_as_python)
self.json_convert_button = QPushButton("Save as JSON")
self.json_convert_button.clicked.connect(self.save_as_json)
# Layout
dataset_layout = QVBoxLayout()

Expand All @@ -303,6 +306,10 @@ def __init__(self, parent):
geopackage_row.addWidget(self.copy_geopackage_button)
geopackage_row.addWidget(self.restore_geopackage_button)
geopackage_layout.addLayout(geopackage_row)
convert_row = QHBoxLayout()
convert_row.addWidget(self.python_convert_button)
convert_row.addWidget(self.json_convert_button)
geopackage_layout.addLayout(convert_row)
dataset_layout.addWidget(geopackage_group)

# Transient versus steady-state selector
Expand All @@ -317,7 +324,6 @@ def __init__(self, parent):
layer_row.addWidget(self.add_button)
layer_row.addWidget(self.remove_button)
dataset_layout.addLayout(layer_row)
dataset_layout.addWidget(self.convert_button)
self.setLayout(dataset_layout)
self.validation_dialog = None

Expand Down Expand Up @@ -353,7 +359,7 @@ def add_item_to_qgis(self, item) -> None:
extent = feature.geometry().boundingBox()
ymax = extent.yMaximum()
ymin = extent.yMinimum()
self.parent.set_cellsize_from_domain(ymax, ymin)
self.parent.set_spacing_from_domain(ymax, ymin)
return

def add_selection_to_qgis(self) -> None:
Expand Down Expand Up @@ -560,13 +566,12 @@ def _extract_data(self, transient: bool) -> Extraction:

return Extraction(timml=timml_data, ttim=ttim_data)

def convert_to_python(self) -> None:
transient = self.transient
def save_as_python(self) -> None:
outpath, _ = QFileDialog.getSaveFileName(self, "Select file", "", "*.py")
if outpath == "": # Empty string in case of cancel button press
return

extraction = self._extract_data(transient=transient)
extraction = self._extract_data(transient=self.transient)
if not extraction.success:
return

Expand All @@ -584,19 +589,18 @@ def convert_to_python(self) -> None:
def convert_to_json(
self,
path: str,
cellsize: float,
transient: bool,
output_options: Dict[str, bool],
output_options: OutputOptions,
) -> bool:
"""
Parameters
----------
path: str
Path to JSON file to write.
cellsize: float
Cell size to use to compute the head grid.
transient: bool
Steady-state (False) or transient (True).
output_options: OutputOptions
Which outputs to compute and write.
Returns
-------
Expand All @@ -610,7 +614,6 @@ def convert_to_json(
json_data = data_to_json(
extraction.timml,
extraction.ttim,
cellsize=cellsize,
output_options=output_options,
)

Expand All @@ -632,3 +635,11 @@ def convert_to_json(
level=Qgis.Info,
)
return False

def save_as_json(self) -> None:
outpath, _ = QFileDialog.getSaveFileName(self, "Select file", "", "*.json")
if outpath == "": # Empty string in case of cancel button press
return

self.convert_to_json(outpath, transient=self.transient, output_options=None)
return
7 changes: 4 additions & 3 deletions plugin/qgistim/widgets/tim_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ def set_interpreter_interaction(self, value: bool) -> None:
mean time.
"""
self.compute_widget.compute_button.setEnabled(value)
self.dataset_widget.convert_button.setEnabled(value)
self.dataset_widget.python_convert_button.setEnabled(value)
self.dataset_widget.json_convert_button.setEnabled(value)
self.extraction_widget.extract_button.setEnabled(value)
return

Expand All @@ -292,8 +293,8 @@ def crs(self) -> Any:
def transient(self) -> bool:
return self.compute_widget.transient

def set_cellsize_from_domain(self, ymax: float, ymin: float) -> None:
self.compute_widget.set_cellsize_from_domain(ymax, ymin)
def set_spacing_from_domain(self, ymax: float, ymin: float) -> None:
self.compute_widget.set_spacing_from_domain(ymax, ymin)

def toggle_element_buttons(self, state: bool) -> None:
self.elements_widget.toggle_element_buttons(state)
Expand Down

0 comments on commit 4f37f78

Please sign in to comment.