Compare commits

...

41 Commits

Author SHA1 Message Date
Pierre-Antoine b666162bdf Merge branch 'scenario-dev-pa' into scenarios. 2025-11-20 15:44:52 +01:00
Pierre-Antoine d705accaed Wiki: Add scenarios screenshot. 2025-11-20 09:58:30 +01:00
Pierre-Antoine 2c49e991b4 GeoTIFF: Fix coordinates. 2025-11-19 11:46:22 +01:00
Pierre-Antoine f8a41fce08 GeoTIFF: Add red rectangle at GeoTIFF selected. 2025-11-17 15:01:59 +01:00
Pierre-Antoine 624ae826eb GeoTIFF: Fix plot display where geotiff is deleted and fix update propagation. 2025-11-17 10:08:06 +01:00
Pierre-Antoine d47dc0687e GeoTIFF: Minor change. 2025-11-14 15:56:24 +01:00
Pierre-Antoine 117e5222e4 GeoTIFF: Edit: Fix event connection. 2025-11-14 15:50:24 +01:00
Pierre-Antoine b1c7a77f37 GeoTIFF: Fix pep8. 2025-11-14 15:18:39 +01:00
Pierre-Antoine 7b833390f1 Results: Add study geotiff in plot xy. 2025-11-14 15:05:00 +01:00
Pierre-Antoine ae08642116 GeoTIFF: Factorise draw geotiff function. 2025-11-14 15:04:31 +01:00
Pierre-Antoine 97ece018aa Reservoirs: Fix loading without nodes. 2025-11-14 10:01:56 +01:00
Pierre-Antoine 7f0102a881 GeoTIFF: Add geotiff to mainwindow tab info and fix bounds at file import. 2025-11-14 09:56:46 +01:00
Pierre-Antoine 786923bdbf GeoTIFF: Add missing ui files. 2025-11-13 15:08:50 +01:00
Pierre-Antoine deb9b2069f GeoTIFF: Integrate geotiff menu to mainwindow and fix bounds update. 2025-11-13 14:56:38 +01:00
Pierre-Antoine 9308a73e8e GeoTIFF: Minor change. 2025-11-13 11:47:51 +01:00
Pierre-Antoine 869e116ad0 GeoTIFF: Minor change. 2025-11-13 10:49:22 +01:00
Pierre-Antoine a2f3d22001 GeoTIFF: Add geometry and geotiff diplay next to the geotiff list. 2025-11-13 10:44:23 +01:00
Pierre-Antoine 2e360943b2 GeoTIFF: Switch PlotXY to display all river reaches. 2025-11-12 11:42:46 +01:00
Pierre-Antoine 8976f054c7 GeoTIFF: Save geotiff into study db. 2025-11-12 11:16:28 +01:00
Pierre-Antoine 7bce725c63 GeoTIFF: Fix pep8. 2025-11-12 10:43:20 +01:00
Pierre-Antoine 5c83d67865 GeoTIFF: Continue intergation. 2025-11-12 10:41:45 +01:00
Pierre-Antoine 6e52b1681e GeoTIFF: Continue view integration. 2025-11-11 12:26:50 +01:00
Pierre-Antoine f27b2cc586 GeoTIFF: Prepare model/view integration. 2025-11-10 17:10:01 +01:00
Pierre-Antoine 1ed5d69bf4 GeoTIFF: Prepare view integration. 2025-11-10 16:28:21 +01:00
Pierre-Antoine 5bb6cc40fe GeoTIFF: Add to modules list. 2025-11-10 16:04:18 +01:00
Pierre-Antoine 4c0a12dcf9 GeoTIFF: Integrate to River submodel. 2025-11-10 10:21:40 +01:00
Pierre-Antoine a308af41e0 GeoTIFF: Prepare add geotiff into pamhyr2 db. 2025-11-07 17:12:27 +01:00
Pierre-Antoine 4cf4015579 Results: Set import data methode default dir as results dir. 2025-11-06 09:30:29 +01:00
Pierre-Antoine 291b97ac9b Results: Select solver for last results also if the results path exists. 2025-11-04 16:38:35 +01:00
Pierre-Antoine b04e367e72 Results: Add InputDialog to select solver. 2025-11-04 16:22:11 +01:00
Pierre-Antoine 81d58122d6 MainWindow: Fix save callback. 2025-11-04 14:34:32 +01:00
Pierre-Antoine 874f592cf4 Results: Fix add data settings. 2025-11-04 14:18:00 +01:00
Pierre-Antoine 9b0bdd1e63 Results: Fix plot add data 'Q(t)'. 2025-11-04 10:15:37 +01:00
Pierre-Antoine 14549330b9 Results: Add additional data to SQL study save. 2025-11-03 16:55:36 +01:00
Pierre-Antoine 2487bec6d6 Results: Minor change. 2025-11-03 15:04:05 +01:00
Pierre-Antoine 4e1acfecdc Results: Refacto read csv. 2025-11-03 14:55:42 +01:00
Pierre-Antoine c63c776989 MainWindow: Add message box at results opening error. 2025-11-03 10:03:49 +01:00
Pierre-Antoine 44a8be6be3 WaitingDialog: Minor change. 2025-10-31 17:27:47 +01:00
Pierre-Antoine a5a64f2080 MainWindow: Minor change and fix. 2025-10-31 15:41:25 +01:00
Pierre-Antoine 16ee5a90e4 Results: Fix update table creation. 2025-10-31 15:05:38 +01:00
Pierre-Antoine 7ca6e69526 Pamhyr2: Add save action (ctrl+s) in each pamhyr window. 2025-10-31 14:43:01 +01:00
27 changed files with 2034 additions and 83 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,368 @@
# GeoTIFF.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import struct
import logging
from functools import reduce
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
logger = logging.getLogger()
class GeoTIFF(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, enabled=True,
name="", description="",
path="", coordinates=None,
status=None, owner_scenario=-1):
super(GeoTIFF, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._enabled = enabled
self._name = f"GeoTIFF #{self._pamhyr_id}" if name == "" else name
self._description = description
self._file_bytes = b''
if coordinates is None:
self._coordinates = {
"bottom": 0.0,
"top": 0.0,
"left": 0.0,
"right": 0.0,
}
else:
self._coordinates = coordinates
self._file_name = ""
if path != "":
self.read_file(path)
self._memfile = None
def __getitem__(self, key):
value = None
if key == "enabled":
value = self._enabled
elif key == "name":
value = self.name
elif key == "description":
value = self.description
elif key == "file_name":
value = self.file_name
elif key == "coordinates":
value = self.coordinates
elif key == "coordinates_bottom":
value = self.coordinates["bottom"]
elif key == "coordinates_top":
value = self.coordinates["top"]
elif key == "coordinates_left":
value = self.coordinates["left"]
elif key == "coordinates_right":
value = self.coordinates["right"]
elif key == "memfile":
value = self.memfile
return value
def __setitem__(self, key, value):
if key == "enabled":
self.enabled = value
elif key == "name":
self.name = value
elif key == "description":
self.description = value
elif key == "file_name":
if self._file_name != value:
self.read_file(value)
elif key == "coordinates":
self.coordinates = value
elif key == "coordinates_bottom" or key == "bottom":
self.coordinates["bottom"] = value
elif key == "coordinates_top" or key == "top":
self.coordinates["top"] = value
elif key == "coordinates_left" or key == "left":
self.coordinates["left"] = value
elif key == "coordinates_right" or key == "right":
self.coordinates["right"] = value
self.modified()
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self.modified()
def is_enabled(self):
return self._enabled
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def description(self):
return self._description
@description.setter
def description(self, description):
self._description = description
self.modified()
@property
def file_name(self):
return self._file_name
@file_name.setter
def file_name(self, file_name):
self._file_name = file_name
self.modified()
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.modified()
@property
def coord_bottom(self):
if self._coordinates is None:
return 0.0
return self._coordinates["bottom"]
@property
def coord_top(self):
if self._coordinates is None:
return 0.0
return self._coordinates["top"]
@property
def coord_left(self):
if self._coordinates is None:
return 0.0
return self._coordinates["left"]
@property
def coord_right(self):
if self._coordinates is None:
return 0.0
return self._coordinates["right"]
@property
def memfile(self):
if not _rasterio_loaded:
return None
if self._file_bytes == b'':
return None
if self._memfile is None:
self._memfile = MemoryFile()
self._memfile.write(self._file_bytes)
return self._memfile
def read_file(self, path):
logger.debug(f"Read GeoTIFF file at : '{path}'")
self._file_name = path
self._file_bytes = b''
self._memfile = None
nbytes = 0
with open(path, "rb") as f:
while True:
data = f.read(4096)
if not data:
break
nbytes += len(data)
self._file_bytes += data
logger.debug(f"Read GeoTIFF: {nbytes} bytes readed")
def write_file(self, path):
with open(path, "w+b") as f:
f.write(self._file_bytes)
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE geotiff{ext} (
{cls.create_db_add_pamhyr_id()},
enabled BOOLEAN NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
description TEXT NOT NULL,
file_name TEXT NOT NULL,
file_bytes BLOB NOT NULL,
coordinates_bottom REAL NOT NULL,
coordinates_top REAL NOT NULL,
coordinates_left REAL NOT NULL,
coordinates_right REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
if major == "0" and int(minor) == 2:
if int(release) < 3:
cls._db_create(execute)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, enabled, deleted, " +
"name, description, file_name, file_bytes, " +
"coordinates_bottom, coordinates_top, " +
"coordinates_left, coordinates_right, " +
"scenario " +
"FROM geotiff " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
it = iter(row)
id = next(it)
enabled = (next(it) == 1)
deleted = (next(it) == 1)
name = next(it)
description = next(it)
file_name = next(it)
file_bytes = next(it)
coordinates_bottom = next(it)
coordinates_top = next(it)
coordinates_left = next(it)
coordinates_right = next(it)
owner_scenario = next(it)
f = cls(
id=id, enabled=enabled, name=name,
description=description, coordinates={
"bottom": coordinates_bottom,
"top": coordinates_top,
"left": coordinates_left,
"right": coordinates_right,
},
status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
f.set_as_deleted()
f._file_bytes = file_bytes
loaded.add(id)
new.append(f)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT INTO geotiff (" +
"pamhyr_id, enabled, deleted, " +
"name, description, file_name, file_bytes, " +
"coordinates_bottom, coordinates_top, " +
"coordinates_left, coordinates_right, " +
"scenario) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
self._pamhyr_id,
self._enabled,
self.is_deleted(),
self.name,
self.description,
self.file_name,
self._file_bytes,
self.coordinates['bottom'],
self.coordinates['top'],
self.coordinates['left'],
self.coordinates['right'],
self._status.scenario_id,
)
return True

View File

@ -0,0 +1,59 @@
# GeoTIFFList.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.GeoTIFF.GeoTIFF import GeoTIFF
class GeoTIFFList(PamhyrModelList):
_sub_classes = [GeoTIFF]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"])
new._lst = GeoTIFF._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
ok = True
# Delete previous data
execute(
"DELETE FROM geotiff " +
f"WHERE scenario = {self._status.scenario_id}"
)
for gt in self._lst:
ok &= gt._db_save(execute, data)
return ok
@property
def files(self):
return self.lst
def new(self, index):
n = GeoTIFF(status=self._status)
self.insert(index, n)
self._status.modified()
return n

View File

@ -310,7 +310,7 @@ class Reservoir(SQLSubModel):
new_reservoir.set_as_deleted()
new_reservoir._node = None
if node_id != -1:
if node_id != -1 and node_id is not None:
new_reservoir._node = next(
filter(
lambda n: n.id == node_id, data["nodes"]

View File

@ -29,8 +29,134 @@ from Model.Results.River.River import River
logger = logging.getLogger()
class AdditionalData(SQLSubModel):
_sub_classes = []
def __init__(self, id=-1, study=None, data=None):
super(AdditionalData, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
)
self._study = study
self._data = data
@property
def data(self):
return self._data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_add_data{ext} (
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
type_x TEXT NOT NULL,
type_y TEXT NOT NULL,
legend TEXT NOT NULL,
unit TEXT NOT NULL,
data_len INTEGER NOT NULL,
x BLOB NOT NULL,
y BLOB NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(result) REFERENCES results(pamhyr_id),
PRIMARY KEY(pamhyr_id, result, scenario)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) == 2 and int(release) <= 1:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
study = data['study']
status = data['status']
scenario = data["scenario"]
table = execute(
"SELECT pamhyr_id, type_x, type_y, " +
"legend, unit, data_len, x, y, " +
"scenario " +
"FROM results_add_data " +
f"WHERE scenario = {scenario.id}"
)
if table is None:
return new
for v in table:
it = iter(v)
pid = next(it)
type_x = next(it)
type_y = next(it)
legend = next(it)
unit = next(it)
data_len = next(it)
bx = next(it)
by = next(it)
owner_scenario = next(it)
data_format = ">" + ''.join(itertools.repeat("d", data_len))
x = struct.unpack(data_format, bx)
y = struct.unpack(data_format, by)
data = {
'type_x': type_x,
'type_y': type_y,
'legend': legend,
'unit': unit,
'x': x, 'y': y
}
new_data = cls(study=study)
new_data._data = data
new.append(new_data)
return new
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
pid = self._pamhyr_id
data_len = len(self._data["x"])
data_format = ">" + ''.join(itertools.repeat("d", data_len))
bx = struct.pack(data_format, *self._data["x"])
by = struct.pack(data_format, *self._data["y"])
execute(
"INSERT INTO " +
"results_add_data (pamhyr_id, result, " +
"type_x, type_y, " +
"legend, unit, data_len, x, y, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, data["result"],
self._data["type_x"], self._data["type_y"],
self._data["legend"], self._data["unit"],
data_len, bx, by, self._owner_scenario
)
return True
class Results(SQLSubModel):
_sub_classes = [River]
_sub_classes = [River, AdditionalData]
def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0"):
@ -50,6 +176,7 @@ class Results(SQLSubModel):
# Keep results creation date
"creation_date": datetime.now(),
"study_revision": study.status.version,
"additional_data": [],
}
if solver is not None:
@ -184,6 +311,11 @@ class Results(SQLSubModel):
data["timestamps"] = sorted(ts)
new_results._river = River._db_load(execute, data)
new_results.set(
"additional_data",
AdditionalData._db_load(execute, data)
)
yield (solver_type, new_results)
def _db_save_clear(self, execute, solver_type, data=None):
@ -206,6 +338,11 @@ class Results(SQLSubModel):
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
execute(
"DELETE FROM results_add_data " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
@ -238,4 +375,7 @@ class Results(SQLSubModel):
data["result"] = self._pamhyr_id
self._river._db_save(execute, data)
for add_data in self.get("additional_data"):
add_data._db_save(execute, data)
return True

View File

@ -105,16 +105,6 @@ class Profile(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
create = False
if major == "0" and int(minor) < 2:
cls._db_create(execute)
create = True
if major == "0" and int(minor) == 2:
if int(release) < 1 and not create:
cls._db_create(execute)
create = True
return cls._update_submodel(execute, version, data)

View File

@ -59,6 +59,7 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
import LateralContributionsAdisTSList
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Results.Results import Results
logger = logging.getLogger()
@ -468,6 +469,7 @@ class River(Graph):
LateralContributionsAdisTSList,
D90AdisTSList,
DIFAdisTSList,
GeoTIFFList,
Results
]
@ -505,6 +507,8 @@ class River(Graph):
self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._geotiff = GeoTIFFList(status=self._status)
self._results = {}
@classmethod
@ -617,6 +621,8 @@ class River(Graph):
new._DIFAdisTS = DIFAdisTSList._db_load(execute, data)
new._geotiff = GeoTIFFList._db_load(execute, data)
return new
def _db_load_results(self, execute, data=None):
@ -650,6 +656,8 @@ class River(Graph):
objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS)
objs.append(self._geotiff)
for solv_type in self.results:
objs.append(self.results[solv_type])
@ -726,6 +734,7 @@ class River(Graph):
self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS,
self._geotiff,
]
for solver in self._parameters:
@ -818,6 +827,10 @@ Last export at: @date."""
def additional_files(self):
return self._additional_files
@property
def geotiff(self):
return self._geotiff
@property
def rep_lines(self):
return self._rep_lines

View File

@ -37,7 +37,7 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.1"
_version = "0.2.3"
_sub_classes = [
Scenario,

View File

@ -56,6 +56,7 @@ class Modules(IterableFlag):
SEDIMENT_LAYER = auto()
ADDITIONAL_FILES = auto()
OUTPUT_RK = auto()
GEOTIFF = auto()
# Results
RESULTS = auto()
@ -81,6 +82,7 @@ class Modules(IterableFlag):
cls.RESULTS,
cls.WINDOW_LIST,
cls.OUTPUT_RK,
cls.GEOTIFF
]
@classmethod
@ -99,6 +101,7 @@ class Modules(IterableFlag):
| cls.HYDRAULIC_STRUCTURES
| cls.RESERVOIR
| cls.SEDIMENT_LAYER
| cls.GEOTIFF
)
@classmethod
@ -114,6 +117,7 @@ class Modules(IterableFlag):
cls.HYDRAULIC_STRUCTURES,
cls.RESERVOIR,
cls.SEDIMENT_LAYER,
cls.GEOTIFF,
]
@classmethod
@ -129,6 +133,7 @@ class Modules(IterableFlag):
cls.HYDRAULIC_STRUCTURES: "Hydraulic structures",
cls.RESERVOIR: "Reservoir",
cls.SEDIMENT_LAYER: "Sediment layer",
cls.GEOTIFF: "GeoTIFF",
}
def impact(self):
@ -168,4 +173,5 @@ _impact = {
Modules.HYDRAULIC_STRUCTURES: [],
Modules.RESERVOIR: [],
Modules.SEDIMENT_LAYER: [],
Modules.GEOTIFF: [],
}

View File

@ -17,6 +17,7 @@
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
@ -92,7 +93,8 @@ class ReplWindow(PamhyrWindow):
value = exec(rich_code)
value = self.__debug_exec_result__
except Exception as e:
value = f"<font color=\"red\">" + str(e) + "</font>"
value = f"<font color=\"red\">" + str(e) + "</font>\n"
value += f"<font color=\"grey\">{traceback.format_exc()}</font>"
# Display code
msg = f"<font color=\"grey\"> # " + code + " #</font>"

View File

@ -0,0 +1,289 @@
# Window.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from Modules import Modules
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtWidgets import (
QLabel, QPlainTextEdit, QPushButton, QCheckBox,
QFileDialog, QVBoxLayout, QDoubleSpinBox,
)
from PyQt5.QtCore import (
QSettings
)
from View.GeoTIFF.Translate import GeoTIFFTranslate
from View.GeoTIFF.UndoCommand import (
SetCommand
)
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.PlotXY import PlotXY
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
logger = logging.getLogger()
class EditGeoTIFFWindow(PamhyrWindow):
_pamhyr_ui = "GeoTIFF"
_pamhyr_name = "Edit GeoTIFF"
def __init__(self, study=None, config=None, geotiff=None,
trad=None, undo=None, parent=None):
super(EditGeoTIFFWindow, self).__init__(
title=self._pamhyr_name,
study=study,
config=config,
trad=trad,
options=[],
parent=parent
)
self._geotiff = geotiff
self._file_name = geotiff.file_name
self._hash_data.append(self._geotiff)
self._undo = undo
self.setup_values()
self.setup_graph()
self.setup_connection()
def setup_graph(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.plot_layout = self.find(QVBoxLayout,
"verticalLayout_geotiff")
self._toolbar = PamhyrPlotToolbar(
self.canvas, self,
items=["home", "zoom", "save", "iso", "back/forward", "move"]
)
self.plot_layout.addWidget(self._toolbar)
self.plot_layout.addWidget(self.canvas)
self.plot = PlotXY(
canvas=self.canvas,
data=self._study.river.enable_edges(),
trad=self._trad,
toolbar=None,
parent=self
)
self.plot.update()
self._plot_img = None
memfile = self._geotiff.memfile
if memfile is not None:
self.draw_geotiff(memfile=memfile)
def setup_values(self):
self.set_check_box("checkBox", self._geotiff.enabled)
self.set_line_edit_text("lineEdit_name", self._geotiff.name)
self.set_line_edit_text("lineEdit_description",
self._geotiff.description)
bounds = list(self._geotiff.coordinates.values())
self._set_values_from_bounds(bounds)
self._set_default_values_from_bounds(bounds)
self._reset_spinboxes()
if self._study.is_read_only():
self.set_check_box_enable("checkBox", False)
self.set_line_edit_enable("lineEdit_name", False)
self.set_line_edit_enable("lineEdit_path", False)
self.set_plaintext_edit_enable("plainTextEdit", False)
def _set_values_from_bounds(self, bounds):
self._values = {
"bottom": bounds[0],
"top": bounds[1],
"left": bounds[2],
"right": bounds[3],
}
def _set_default_values_from_bounds(self, bounds):
self._values_default = {
"bottom": bounds[0],
"top": bounds[1],
"left": bounds[2],
"right": bounds[3],
}
def _reset_spinboxes(self):
for key in self._values:
self._reset_spinbox(key)
def _reset_spinbox(self, key):
self.set_double_spin_box(
f"doubleSpinBox_{key}", self._values_default[key]
)
def setup_connection(self):
self.find(QPushButton, "pushButton_cancel")\
.clicked.connect(self.close)
self.find(QPushButton, "pushButton_ok")\
.clicked.connect(self.accept)
self.find(QPushButton, "pushButton_import")\
.clicked.connect(self._import)
self.find(QPushButton, "pushButton_bottom")\
.clicked.connect(lambda: self._reset_spinbox("bottom"))
self.find(QPushButton, "pushButton_top")\
.clicked.connect(lambda: self._reset_spinbox("top"))
self.find(QPushButton, f"pushButton_left")\
.clicked.connect(lambda: self._reset_spinbox("left"))
self.find(QPushButton, f"pushButton_right")\
.clicked.connect(lambda: self._reset_spinbox("right"))
self.find(QDoubleSpinBox, f"doubleSpinBox_bottom")\
.valueChanged.connect(
lambda: self.update_values_from_spinbox("bottom")
)
self.find(QDoubleSpinBox, f"doubleSpinBox_top")\
.valueChanged.connect(
lambda: self.update_values_from_spinbox("top")
)
self.find(QDoubleSpinBox, f"doubleSpinBox_left")\
.valueChanged.connect(
lambda: self.update_values_from_spinbox("left")
)
self.find(QDoubleSpinBox, f"doubleSpinBox_right")\
.valueChanged.connect(
lambda: self.update_values_from_spinbox("right")
)
def update_values_from_spinbox(self, key):
self._values[key] = self.get_double_spin_box(f"doubleSpinBox_{key}")
left = self._values["left"]
right = self._values["right"]
bottom = self._values["bottom"]
top = self._values["top"]
self._plot_img.set_extent((left, right, bottom, top))
self.plot.idle()
def draw_geotiff(self, memfile=None):
if not _rasterio_loaded:
return
if memfile is None:
if self._file_name == "":
return
with rasterio.open(self._file_name) as data:
img = data.read()
b = data.bounds[:] # left, bottom, right, top
if b[2] > b[0] and b[1] < b[3]:
coord = [b[1], b[3], b[0], b[2]]
else:
xlim = self.canvas.axes.get_xlim()
ylim = self.canvas.axes.get_ylim()
coord = ylim + xlim
self._set_values_from_bounds(coord)
self._set_default_values_from_bounds(coord)
else:
with memfile.open() as gt:
img = gt.read()
if self._plot_img is not None:
self._plot_img.remove()
left = self._values["left"]
right = self._values["right"]
bottom = self._values["bottom"]
top = self._values["top"]
self._plot_img = self.canvas.axes.imshow(
img.transpose((1, 2, 0)),
extent=(left, right, bottom, top)
)
self.plot.idle()
self._reset_spinboxes()
def _import(self):
options = QFileDialog.Options()
settings = QSettings(QSettings.IniFormat,
QSettings.UserScope, 'MyOrg', )
options |= QFileDialog.DontUseNativeDialog
file_types = [
self._trad["file_geotiff"],
self._trad["file_all"],
]
filename, _ = QFileDialog.getOpenFileName(
self,
self._trad["open_file"],
"",
";; ".join(file_types),
options=options
)
if filename != "":
self._file_name = filename
self.draw_geotiff()
def accept(self):
if self._study.is_editable():
is_enabled = self.get_check_box("checkBox")
name = self.get_line_edit_text("lineEdit_name")
description = self.get_line_edit_text("lineEdit_description")
coord_bottom = self.get_double_spin_box("doubleSpinBox_bottom")
coord_top = self.get_double_spin_box("doubleSpinBox_top")
coord_left = self.get_double_spin_box("doubleSpinBox_left")
coord_right = self.get_double_spin_box("doubleSpinBox_right")
self._undo.push(
SetCommand(
self._geotiff, enabled=is_enabled,
name=name, description=description,
coordinates_bottom=coord_bottom,
coordinates_top=coord_top,
coordinates_left=coord_left,
coordinates_right=coord_right,
file_name=self._file_name,
)
)
self._propagate_update(key=Modules.GEOTIFF)
self.close()

96
src/View/GeoTIFF/List.py Normal file
View File

@ -0,0 +1,96 @@
# List.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from functools import reduce
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant,
)
from PyQt5.QtGui import (
QColor, QBrush,
)
from View.Tools.PamhyrList import PamhyrListModel
from View.GeoTIFF.UndoCommand import (
AddCommand, DelCommand
)
logger = logging.getLogger()
class ListModel(PamhyrListModel):
def get_true_data_row(self, row):
el = self._data.get(row)
return next(
map(
lambda e: e[0],
filter(
lambda e: e[1] == el,
enumerate(self._data._lst)
)
), 0
)
def data(self, index, role):
row = index.row()
column = index.column()
file = self._data.files[row]
if role == Qt.ForegroundRole:
color = Qt.gray
if file.is_enabled():
color = QColor("black")
else:
color = QColor("grey")
return QBrush(color)
if role == Qt.ItemDataRole.DisplayRole:
text = f"{file.name}: '{file.description}'"
if not file.is_enabled():
text += " (disabled)"
return text
return QVariant()
def add(self, row):
row = self.get_true_data_row(row)
self._undo.push(
AddCommand(
self._data, row
)
)
self.update()
def delete(self, row):
self._undo.push(
DelCommand(
self._data, self._data.files[row]
)
)
self.update()

View File

@ -0,0 +1,35 @@
# Translate.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class GeoTIFFTranslate(MainTranslate):
def __init__(self):
super(GeoTIFFTranslate, self).__init__()
self._dict["GeoTIFF files"] = _translate(
"GeoTIFF", "GeoTIFF files"
)
self._dict["Edit additional file"] = _translate(
"GeoTIFF", "Edit GeoTIFF file"
)

View File

@ -0,0 +1,81 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
class SetCommand(QUndoCommand):
def __init__(self, geotiff, **kwargs):
QUndoCommand.__init__(self)
self._geotiff = geotiff
self._new = kwargs
self._old = None
def undo(self):
f = self._geotiff
for key in self._old:
f[key] = self._old[key]
def redo(self):
f = self._geotiff
if self._old is None:
self._old = {}
for key in self._new:
self._old[key] = f[key]
for key in self._new:
f[key] = self._new[key]
class AddCommand(QUndoCommand):
def __init__(self, files, row):
QUndoCommand.__init__(self)
self._files = files
self._row = row
self._new = None
def undo(self):
self._new.set_as_deleted()
def redo(self):
if self._new is None:
self._new = self._files.new(self._row)
else:
self._new.set_as_not_deleted()
class DelCommand(QUndoCommand):
def __init__(self, files, line):
QUndoCommand.__init__(self)
self._files = files
self._line = line
def undo(self):
self._line.set_as_not_deleted()
def redo(self):
self._line.set_as_deleted()

217
src/View/GeoTIFF/Window.py Normal file
View File

@ -0,0 +1,217 @@
# Window.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from PyQt5.QtWidgets import (
QAction, QListView, QVBoxLayout,
)
from matplotlib.patches import Rectangle
from Modules import Modules
from View.Tools.PamhyrWindow import PamhyrWindow
from View.GeoTIFF.List import ListModel
from View.GeoTIFF.Translate import GeoTIFFTranslate
from View.GeoTIFF.Edit.Window import EditGeoTIFFWindow
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.PlotXY import PlotXY
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
class GeoTIFFListWindow(PamhyrWindow):
_pamhyr_ui = "GeoTIFFList"
_pamhyr_name = "GeoTIFF files"
def __init__(self, study=None, config=None,
parent=None):
trad = GeoTIFFTranslate()
name = trad[self._pamhyr_name] + " - " + study.name
super(GeoTIFFListWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
options=[],
parent=parent
)
self.setup_list()
self.setup_graph()
self.setup_connections()
def setup_list(self):
lst = self.find(QListView, "listView")
self._list = ListModel(
list_view=lst,
data=self._study.river.geotiff,
undo=self._undo_stack,
trad=self._trad,
)
def setup_graph(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self._plot_layout = self.find(QVBoxLayout, "verticalLayout")
self._toolbar = PamhyrPlotToolbar(
self.canvas, self,
items=["home", "zoom", "save", "iso", "back/forward", "move"]
)
self._plot_layout.addWidget(self._toolbar)
self._plot_layout.addWidget(self.canvas)
self._plot = PlotXY(
canvas=self.canvas,
data=self._study.river.enable_edges(),
geotiff=self._study.river.geotiff,
trad=self._trad,
toolbar=None,
parent=self
)
self._plot_rect = []
self._plot.update()
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_delete").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
self.find(QListView, "listView")\
.selectionModel()\
.selectionChanged\
.connect(self._update_rectangle)
def _propagated_update(self, key=Modules(0)):
if Modules.GEOMETRY not in key and Modules.GEOTIFF not in key:
return
self.update()
def update(self):
self._list.update()
self._plot.update()
self._update_rectangle()
def _update_rectangle(self):
for rect in self._plot_rect:
rect.remove()
self._plot_rect = []
rows = self.selected_rows()
if len(rows) <= 0:
return
for row in rows:
files = self._study.river.geotiff.files
if len(files) <= row:
continue
geotiff = files[row]
coord = geotiff.coordinates
xy = (coord["left"], coord["bottom"])
width = abs(coord["right"] - coord["left"])
height = abs(coord["top"] - coord["bottom"])
rect = Rectangle(
xy, width, height,
edgecolor='red', facecolor='none',
lw=2
)
self._plot_rect.append(rect)
self.canvas.axes.add_patch(
rect
)
self._plot.idle()
def selected_rows(self):
lst = self.find(QListView, f"listView")
return list(map(lambda i: i.row(), lst.selectedIndexes()))
def add(self):
rows = self.selected_rows()
if len(rows) > 0:
row = rows[0]
else:
row = 0
self._list.add(row)
def delete(self):
rows = self.selected_rows()
if len(rows) == 0:
return
self._list.delete(rows[0])
self.update()
def edit(self):
rows = self.selected_rows()
for row in rows:
geotiff = self._study.river.geotiff.files[row]
if self.sub_window_exists(
EditGeoTIFFWindow,
data=[self._study, self._config, geotiff]
):
continue
win = EditGeoTIFFWindow(
study=self._study,
config=self._config,
geotiff=geotiff,
trad=self._trad,
undo=self._undo_stack,
parent=self,
)
win.show()
def _undo(self):
self._undo_stack.undo()
self.update()
def _redo(self):
self._undo_stack.redo()
self.update()

View File

@ -47,7 +47,7 @@ from PyQt5.QtWidgets import (
QMainWindow, QApplication, QAction,
QFileDialog, QShortcut, QMenu, QToolBar,
QMessageBox, QProgressDialog, QTabWidget,
QDialog, QVBoxLayout, QLabel,
QDialog, QVBoxLayout, QLabel, QInputDialog,
)
from PyQt5.uic import loadUi
@ -79,6 +79,7 @@ from View.Frictions.Window import FrictionsWindow
from View.SedimentLayers.Window import SedimentLayersWindow
from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow
from View.AdditionalFiles.Window import AddFileListWindow
from View.GeoTIFF.Window import GeoTIFFListWindow
from View.REPLines.Window import REPLineListWindow
from View.SolverParameters.Window import SolverParametersWindow
from View.RunSolver.Window import (
@ -157,7 +158,7 @@ define_model_action = [
"action_menu_boundary_conditions_sediment",
"action_menu_rep_additional_lines", "action_menu_output_rk",
"action_menu_run_adists", "action_menu_pollutants",
"action_menu_d90", "action_menu_dif",
"action_menu_d90", "action_menu_dif", "action_menu_edit_geotiff"
]
action = (
@ -276,8 +277,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_new": self.open_new_study,
"action_menu_edit": self.open_edit_study,
"action_menu_open": self.open_model,
"action_menu_save": self.save_study,
"action_menu_save_as": self.save_as_study,
"action_menu_save": lambda: self.save_study(),
"action_menu_save_as": lambda: self.save_as_study(),
"action_menu_numerical_parameter": self.open_solver_parameters,
"action_menu_edit_scenarios": self.open_scenarios,
"action_menu_edit_network": self.open_network,
@ -297,6 +298,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.open_reach_sediment_layers,
"action_menu_additional_file": self.open_additional_files,
"action_menu_rep_additional_lines": self.open_rep_lines,
"action_menu_edit_geotiff": self.open_geotiff,
"action_menu_close": self.close_model,
"action_menu_results_last": self.open_last_results,
"action_menu_open_results_from_file": self.open_results_from_file,
@ -313,7 +315,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_about": self.open_about,
# ToolBar action
"action_toolBar_open": self.open_model,
"action_toolBar_save": self.save_study,
"action_toolBar_save": lambda: self.save_study(),
"action_toolBar_close": self.close_model,
"action_toolBar_run_solver": self.run_lasest_solver,
# Current actions
@ -606,6 +608,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
return None
def get_last_results(self, solver):
if self._study is None:
return None
results = self._study.results
if solver in results:
return self._study.results[solver]
return None
@last_results.setter
def last_results(self, results):
if self._study is None:
@ -637,7 +649,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
logger.info(f"Open Study - {self._study.name}")
self.set_title()
def save_study(self):
def _save(self, source):
self.save_study(progress_parent=source)
def save_study(self, progress_parent=None):
"""Save current study
Save current study, if study as no associate file, open a
@ -667,11 +682,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._backup_timer.blockSignals(True)
self._save_mutex.lock()
parent = self
if progress_parent is not None:
parent = progress_parent
sql_request_count = self._study.sql_save_request_count()
progress = QProgressDialog(
"Saving...", None,
0, sql_request_count,
parent=self
parent=parent
)
progress.setWindowModality(Qt.WindowModal)
progress.setValue(0)
@ -684,6 +703,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
progress=lambda: progress.setValue(progress.value() + 1)
)
progress.close()
status += " Done"
logger.info(status)
self.statusbar.showMessage(status, 3000)
@ -758,6 +778,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
progress.setValue(progress.value() + 1)
progress.close()
def save_as_study_single_scenario(self, sid=-1):
sql_request_count = self._study.sql_save_request_count()
@ -782,6 +804,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
status += " Done"
progress.close()
logger.info(status)
self.statusbar.showMessage(status, 3000)
@ -1339,6 +1362,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
self.additonal_files.show()
def open_geotiff(self):
if self._study is not None:
if self.sub_window_exists(
GeoTIFFListWindow,
data=[self._study, None]
):
return
self.geotiff = GeoTIFFListWindow(
study=self._study, parent=self
)
self.geotiff.show()
def open_rep_lines(self):
if self._study is not None:
if self.sub_window_exists(
@ -1523,14 +1559,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# If no specific results, get last results
if results is None:
def reading_fn():
self._tmp_results = self.last_results
self._tmp_results = solver.results(
self._study,
self._solver_workdir(solver),
)
if self.last_results is None:
if solver == self._last_solver:
def reading_fn():
self._tmp_results = solver.results(
self._study,
self._solver_workdir(solver),
)
self._tmp_results = self.last_results
# Open from file
if type(results) is str:
@ -1555,13 +1591,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
dlg.exec_()
results = self._tmp_results
# self.last_results = results
# No results available
if results is None:
self.msg_open_results_no_results()
return
# results does not have values, for example if geometry missmatch
if not results.is_valid:
self.msg_open_results_invalid_results()
return
if results.get('study_revision') != self._study.status.version:
@ -1587,6 +1626,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
res.show()
def msg_open_results_no_results(self):
self.message_box(
window_title=self._trad["Warning"],
text=self._trad["mb_open_results_title"],
informative_text=self._trad["mb_open_results_no_results_msg"]
)
def msg_open_results_invalid_results(self):
self.message_box(
window_title=self._trad["Error"],
text=self._trad["mb_open_results_title"],
informative_text=self._trad["mb_open_results_invalid_results_msg"]
)
def open_solver_results_adists(self, solver, results=None):
def reading_fn():
self._tmp_results = results
@ -1662,16 +1715,49 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
return workdir
def is_solver_workdir_exists(self, solver, scenario=None):
return os.path.exists(
self._solver_workdir(solver, scenario)
)
def open_last_results(self):
if self._last_solver is None:
return
solver_type = self._study.results
solver_name, ok = QInputDialog.getItem(
self, self._trad['Solver'],
self._trad['Solver'] + ":",
list(
map(
lambda s: s.name,
filter(
lambda s: (self.is_solver_workdir_exists(s)
or s._type in solver_type),
self.conf.solvers
)
)
)
)
if not ok:
return
solver = next(
filter(
lambda s: s.name == solver_name,
self.conf.solvers
)
)
if self._last_solver._type == "mage8":
self.open_solver_results(self._last_solver,
self.last_results)
self.open_solver_results(
solver, # self.last_results
)
elif self._last_solver._type == "adistswc":
self.open_solver_results_adists(self._last_solver,
self.last_results)
self.open_solver_results_adists(
solver, # self.last_results
)
def open_results_from_file(self):
if self._study is None:

View File

@ -27,6 +27,20 @@ from View.Tools.PamhyrWidget import PamhyrWidget
from PyQt5.QtWidgets import QVBoxLayout
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
logger = logging.getLogger()
@ -104,10 +118,12 @@ class WidgetInfo(PamhyrWidget):
self.plot = PlotXY(
canvas=self.canvas,
data=self._study.river.enable_edges(),
geotiff=self._study.river.geotiff,
trad=self.parent._trad,
toolbar=self._toolbar_xy,
parent=self
)
self.plot.update()
def set_network_values(self):

View File

@ -27,12 +27,26 @@ from PyQt5.QtCore import (
)
from PyQt5.QtWidgets import QApplication, QTableView
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
_translate = QCoreApplication.translate
class PlotXY(PamhyrPlot):
def __init__(self, canvas=None, trad=None, data=None, toolbar=None,
table=None, parent=None):
def __init__(self, canvas=None, trad=None, data=None, geotiff=None,
toolbar=None, table=None, parent=None):
super(PlotXY, self).__init__(
canvas=canvas,
trad=trad,
@ -43,10 +57,14 @@ class PlotXY(PamhyrPlot):
)
self._data = data
self._geotiff = geotiff
self.label_x = self._trad["x"]
self.label_y = self._trad["y"]
self.parent = parent
self._plot_img = {}
self._isometric_axis = True
self._auto_relim_update = True
@ -69,7 +87,12 @@ class PlotXY(PamhyrPlot):
if data.reach.number_profiles != 0:
self.draw_xy(data.reach)
self.draw_lr(data.reach)
self.idle()
if self._geotiff is not None:
self.draw_geotiff(self._geotiff.files)
self.idle()
return
def draw_xy(self, reach):
@ -78,9 +101,9 @@ class PlotXY(PamhyrPlot):
line_xy.append(np.column_stack(xy))
line_xy_collection = collections.LineCollection(
line_xy,
colors=self.color_plot_river_bottom
)
line_xy,
colors=self.color_plot_river_bottom
)
self.canvas.axes.add_collection(line_xy_collection)
def draw_lr(self, reach):
@ -113,6 +136,42 @@ class PlotXY(PamhyrPlot):
)
self.line_lr.append(line)
def draw_geotiff(self, lst):
if not _rasterio_loaded:
return
for img in self._plot_img:
self._plot_img[img].remove()
self._plot_img = {}
for geotiff in lst:
if geotiff.is_deleted():
continue
memfile = geotiff.memfile
if memfile is None:
continue
with memfile.open() as gt:
img = gt.read()
coords = geotiff.coordinates
left = coords["left"]
right = coords["right"]
bottom = coords["bottom"]
top = coords["top"]
self._plot_img[geotiff] = self.canvas.axes.imshow(
img.transpose((1, 2, 0)),
extent=(left, right, bottom, top)
)
if not geotiff.is_enabled():
self._plot_img[geotiff].set(alpha=0.5)
self.idle()
@timer
def update(self):
self.draw()

View File

@ -31,6 +31,20 @@ from PyQt5.QtCore import (
)
from PyQt5.QtWidgets import QApplication, QTableView
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
_translate = QCoreApplication.translate
logger = logging.getLogger()
@ -52,6 +66,8 @@ class PlotXY(PamhyrPlot):
self.line_gl = []
self.overflow = []
self._plot_img = {}
self._timestamps = parent._timestamps
self._current_timestamp = max(self._timestamps)
self._current_reach_id = reach_id
@ -153,6 +169,7 @@ class PlotXY(PamhyrPlot):
reach = results.river.reach(self._current_reach_id)
reaches = results.river.reachs
self.draw_geotiff()
self.draw_profiles(reach, reaches)
self.draw_water_elevation(reach)
self.draw_water_elevation_max(reach)
@ -166,6 +183,7 @@ class PlotXY(PamhyrPlot):
if reach.geometry.number_profiles == 0:
self._init = False
return
self.line_xy = []
# TODO uncomment to draw all the reaches
# self.draw_other_profiles(reaches)
@ -306,6 +324,41 @@ class PlotXY(PamhyrPlot):
alpha=0.7
)
def draw_geotiff(self):
if not _rasterio_loaded:
return
lst = self._data[0]._study.river._geotiff.lst
for img in self._plot_img:
self._plot_img[img].remove()
self._plot_img = {}
for geotiff in lst:
memfile = geotiff.memfile
if memfile is None:
return
with memfile.open() as gt:
img = gt.read()
coords = geotiff.coordinates
left = coords["left"]
right = coords["right"]
bottom = coords["bottom"]
top = coords["top"]
self._plot_img[geotiff] = self.canvas.axes.imshow(
img.transpose((1, 2, 0)),
extent=(left, right, bottom, top)
)
if not geotiff.is_enabled():
self._plot_img[geotiff].set(alpha=0.5)
self.idle()
def set_reach(self, reach_id):
self._current_reach_id = reach_id
self._current_profile_id = 0

View File

@ -56,9 +56,12 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QSlider, QLabel, QWidget, QGridLayout, QTabBar, QInputDialog
QSlider, QLabel, QWidget, QGridLayout, QTabBar,
QInputDialog,
)
from Model.Results.Results import AdditionalData
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
@ -144,11 +147,14 @@ class ResultsWindow(PamhyrWindow):
profile_id=[0])
self.update_table_selection_solver(0)
self.update_plot_additional_data()
def setup_table(self):
self._table = {}
for t in ["reach", "profile", "raw_data", "solver"]:
table = self.find(QTableView, f"tableView_{t}")
self._table[t] = TableModel(
table_view=table,
table_headers=self._trad.get_dict(f"table_headers_{t}"),
@ -157,8 +163,11 @@ class ResultsWindow(PamhyrWindow):
opt_data=t,
parent=self
)
self._table[t]._timestamp = self._timestamps[
self._slider_time.value()]
self._slider_time.value()
]
if len(self._results) <= 1:
table = self.find(QTableView, f"tableView_solver")
table.hide()
@ -189,12 +198,15 @@ class ResultsWindow(PamhyrWindow):
def setup_plots(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_widget.setTabsClosable(True)
tab_widget.tabCloseRequested.connect(self.delete_tab)
tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, None)
tab_widget.tabBar().setTabButton(1, QTabBar.RightSide, None)
tab_widget.tabBar().setTabButton(2, QTabBar.RightSide, None)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self, items=[
@ -202,6 +214,7 @@ class ResultsWindow(PamhyrWindow):
"iso", "back/forward"
]
)
self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
self.plot_layout.addWidget(self.toolbar)
self.plot_layout.addWidget(self.canvas)
@ -1217,8 +1230,7 @@ class ResultsWindow(PamhyrWindow):
extent=[b[0], b[2], b[1], b[3]])
else:
dlg = CoordinatesDialog(
xlim,
ylim,
xlim, ylim,
trad=self._trad,
parent=self
)
@ -1231,7 +1243,6 @@ class ResultsWindow(PamhyrWindow):
return
def import_data(self):
file_types = [
self._trad["file_csv"],
self._trad["file_all"],
@ -1242,41 +1253,61 @@ class ResultsWindow(PamhyrWindow):
callback=lambda f: self.read_csv_file(f[0]),
default_suffix=".csv",
file_filter=file_types,
directory=self._results[self._current_results[0]]._repertory,
)
def read_csv_file(self, filename):
if filename == "":
return
sep = " "
x, y = self.read_csv_file_data(filename)
data = self.read_csv_file_format(x, y)
def is_float(string):
if string.replace(".", "").isnumeric():
return True
else:
return False
results = self._results[self._current_results[0]]
data_lst = results.get("additional_data")
data_lst.append(
AdditionalData(
study=self._study,
data=data
)
)
self.update_plot_additional_data()
def read_csv_file_data(self, filename):
sep = ","
x = []
y = []
with open(filename, 'r', newline='') as f:
lines = f.readlines()
x = []
y = []
for line in lines:
if line[0] != "*" and line[0] != "#" and line[0] != "$":
row = line.split(sep)
if len(row) >= 2:
if is_float(row[0]) and is_float(row[1]):
x.append(float(row[0]))
y.append(float(row[1]))
if len(row) < 2:
continue
try:
fx, fy = float(row[0]), float(row[1])
x.append(fx)
y.append(fy)
except Exception as e:
continue
return x, y
def read_csv_file_format(self, x, y):
data_type_lst = ['Q(t)', 'Z(t)', 'Z(x)']
data_type, ok = QInputDialog.getItem(
self, 'Data type', 'Chose the type of data:', data_type_lst)
self, 'Data type',
'Chose the type of data:',
data_type_lst
)
if not ok:
return
legend, ok = QInputDialog.getText(self, 'Legend', 'Legend:')
if not ok:
return
@ -1291,25 +1322,45 @@ class ResultsWindow(PamhyrWindow):
tmp_unit = {'Z': ' (m)',
'Q': ' (m³/s)'}
data = {'type_x': tmp_dict[data_type[2]],
'type_y': tmp_dict[data_type[0]],
'legend': legend,
'unit': tmp_unit[data_type[0]],
'x': x,
'y': y}
data = {
'type_x': tmp_dict[data_type[2]],
'type_y': tmp_dict[data_type[0]],
'legend': legend,
'unit': tmp_unit[data_type[0]],
'x': x, 'y': y
}
if data_type == 'Z(x)':
line = self.canvas_2.axes.plot(x, y, marker="+",
label=legend + ' (m)')
self.plot_rkc.canvas.draw_idle()
self.plot_rkc.update_idle()
if data_type == 'Q(t)':
line = self.canvas_4.axes.plot(x, y, marker="+",
label=legend + ' (m³/s)')
self.plot_h._line.append(line)
self.plot_h.enable_legend()
self.plot_h.canvas.draw_idle()
self.plot_h.update_idle
return data
for p in self._additional_plot:
self._additional_plot[p].add_imported_plot(data)
def update_plot_additional_data(self):
results = self._results[self._current_results[0]]
for data in results.get("additional_data"):
data = data._data
x, y = data['x'], data['y']
legend = data['legend']
unit = data['unit']
if (
data['type_x'] == 'water_elevation' and
data['type_y'] == 'time'
):
line = self.canvas_2.axes.plot(
x, y, marker="+",
label=legend + ' ' + unit
)
self.plot_rkc.canvas.draw_idle()
self.plot_rkc.update_idle()
if data['type_x'] == 'time' and data['type_y'] == 'discharge':
line = self.canvas_4.axes.plot(
x, y, marker="+",
label=legend + ' ' + unit
)
self.plot_h._line.append(line)
self.plot_h.enable_legend()
self.plot_h.canvas.draw_idle()
self.plot_h.update_idle()
for p in self._additional_plot:
self._additional_plot[p].add_imported_plot(data)

View File

@ -53,11 +53,6 @@ class ResultsTranslate(MainTranslate):
"Results",
"Max water elevation"
)
self._dict["file_all"] = _translate("Results", "All files (*)")
self._dict["file_geotiff"] = _translate(
"Results", "GeoTIFF file (*.tiff *.tif)")
self._dict["file_csv"] = _translate(
"Results", "CSV file (*.csv)")
self._dict["ImageCoordinates"] = _translate(
"Results", "Image coordinates"
)

View File

@ -182,6 +182,7 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools):
self._set_title()
self._set_icon()
self._setup_save_sc()
def _set_title(self):
title = self._title
@ -196,12 +197,25 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools):
self.ui.setWindowTitle(title)
def _setup_save_sc(self):
if self._parent is None:
return
self._save_sc = QShortcut(QKeySequence("Ctrl+S"), self)
self._save_sc.activated.connect(lambda: self._save(self))
def closeEvent(self, event):
self._close_sub_window()
self._propagate_update(Modules.WINDOW_LIST)
super(PamhyrWindow, self).closeEvent(event)
def _save(self, source):
if self._parent is None:
return
return self._parent._save(source)
class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools):
_pamhyr_ui = "dummy"

View File

@ -54,6 +54,18 @@ class CommonWordTranslate(PamhyrTranslate):
self._dict["method"] = _translate("CommonWord", "Method")
# Files
self._dict["open_file"] = _translate(
"CommonWord", "Open file"
)
self._dict["file_all"] = _translate("CommonWord", "All files (*)")
self._dict["file_geotiff"] = _translate(
"CommonWord", "GeoTIFF file (*.tiff *.tif)"
)
self._dict["file_csv"] = _translate(
"CommonWord", "CSV file (*.csv)"
)
class UnitTranslate(CommonWordTranslate):
def __init__(self):
@ -180,6 +192,9 @@ class MainTranslate(UnitTranslate):
)
# Message box
self._dict["Error"] = _translate(
"MainWindow", "Error"
)
self._dict["Warning"] = _translate(
"MainWindow", "Warning"
)
@ -234,6 +249,17 @@ class MainTranslate(UnitTranslate):
self._dict["mb_diff_results_param_msg"] = _translate(
"MainWindow", "Results comparison parameters is invalid"
)
self._dict["mb_open_results_title"] = _translate(
"MainWindow", "Open results"
)
self._dict["mb_open_results_no_results_msg"] = _translate(
"MainWindow", "No results found"
)
self._dict["mb_open_results_invalid_results_msg"] = _translate(
"MainWindow", "Failed to read results"
)
self._dict["mb_diff_results_compatibility_msg"] = _translate(
"MainWindow",
"Results comparison with two "
@ -247,3 +273,4 @@ class MainTranslate(UnitTranslate):
self._dict["Cancel"] = _translate("MainWindow", "Cancel")
self._dict["Save"] = _translate("MainWindow", "Save")
self._dict["Close"] = _translate("MainWindow", "Close")
self._dict["Solver"] = _translate("MainWindow", "Solver")

View File

@ -63,7 +63,7 @@ class WaitingDialog(PamhyrDialog):
". ", ". ",
".. ", ".. ",
"...", "..."],
["o ", " o ", " o ", " o", " o ", " o "],
["o ", " o ", " o ", " o", " o ", " o "],
["█▓▒░", "▓█▓▒", "▒▓█▓", "░▒▓█", "▒▓█▓", "▓█▓▒"],
"▖▘▝▗",
"αβγδεζηθικλμνξοπρστυφχψω",

264
src/View/ui/GeoTIFF.ui Normal file
View File

@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>896</width>
<height>504</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_ok">
<property name="text">
<string>Ok</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Informations</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_description">
<property name="toolTip">
<string>The relative file path on executable directory</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_name"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>GeoTIFF file</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_import">
<property name="text">
<string>Import GeoTIFF file</string>
</property>
</widget>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout_geotiff"/>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="2">
<widget class="QPushButton" name="pushButton_left">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_right">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Right coordinate</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="pushButton_right">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_left">
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-99999999.000000000000000</double>
</property>
<property name="maximum">
<double>99999999.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="pushButton_bottom">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="pushButton_top">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_right">
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-99999999.000000000000000</double>
</property>
<property name="maximum">
<double>99999999.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_top">
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-99999999.000000000000000</double>
</property>
<property name="maximum">
<double>99999999.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_top">
<property name="text">
<string>Top coordinate</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_bottom">
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-99999999.000000000000000</double>
</property>
<property name="maximum">
<double>99999999.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_left">
<property name="text">
<string>Left coordinate</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_bottom">
<property name="text">
<string>Bottom coordinate</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>896</width>
<height>504</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QListView" name="listView"/>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_delete"/>
<addaction name="action_edit"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add a new file</string>
</property>
</action>
<action name="action_delete">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete selected file(s)</string>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit file</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -129,6 +129,7 @@
<string>&amp;Geometry</string>
</property>
<addaction name="action_menu_edit_geometry"/>
<addaction name="action_menu_edit_geotiff"/>
</widget>
<widget class="QMenu" name="menu_run">
<property name="locale">
@ -811,6 +812,11 @@
<string>Compare results</string>
</property>
</action>
<action name="action_menu_edit_geotiff">
<property name="text">
<string>GeoTIFF</string>
</property>
</action>
</widget>
<resources/>
<connections>