diff --git a/doc/images/wiki/fr_scenarios.png b/doc/images/wiki/fr_scenarios.png
new file mode 100644
index 00000000..96bdd341
Binary files /dev/null and b/doc/images/wiki/fr_scenarios.png differ
diff --git a/src/Model/GeoTIFF/GeoTIFF.py b/src/Model/GeoTIFF/GeoTIFF.py
new file mode 100644
index 00000000..1a9281cd
--- /dev/null
+++ b/src/Model/GeoTIFF/GeoTIFF.py
@@ -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 .
+
+# -*- 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
diff --git a/src/Model/GeoTIFF/GeoTIFFList.py b/src/Model/GeoTIFF/GeoTIFFList.py
new file mode 100644
index 00000000..84f3c803
--- /dev/null
+++ b/src/Model/GeoTIFF/GeoTIFFList.py
@@ -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 .
+
+# -*- 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
diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py
index 1d1998b6..c093691c 100644
--- a/src/Model/Reservoir/Reservoir.py
+++ b/src/Model/Reservoir/Reservoir.py
@@ -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"]
diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py
index 3b352bec..aad79c04 100644
--- a/src/Model/Results/Results.py
+++ b/src/Model/Results/Results.py
@@ -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
diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py
index d111bf9f..bed5fd45 100644
--- a/src/Model/Results/River/River.py
+++ b/src/Model/Results/River/River.py
@@ -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)
diff --git a/src/Model/River.py b/src/Model/River.py
index e0c4602b..ba5f5e8a 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -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
diff --git a/src/Model/Study.py b/src/Model/Study.py
index 2d7292b7..f32e90bf 100644
--- a/src/Model/Study.py
+++ b/src/Model/Study.py
@@ -37,7 +37,7 @@ logger = logging.getLogger()
class Study(SQLModel):
- _version = "0.2.1"
+ _version = "0.2.3"
_sub_classes = [
Scenario,
diff --git a/src/Modules.py b/src/Modules.py
index fecb6322..860bc0cf 100644
--- a/src/Modules.py
+++ b/src/Modules.py
@@ -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: [],
}
diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py
index c3ebae91..c5f4c720 100644
--- a/src/View/Debug/Window.py
+++ b/src/View/Debug/Window.py
@@ -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"" + str(e) + ""
+ value = f"" + str(e) + "\n"
+ value += f"{traceback.format_exc()}"
# Display code
msg = f" # " + code + " #"
diff --git a/src/View/GeoTIFF/Edit/Window.py b/src/View/GeoTIFF/Edit/Window.py
new file mode 100644
index 00000000..fa14292d
--- /dev/null
+++ b/src/View/GeoTIFF/Edit/Window.py
@@ -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 .
+
+# -*- 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()
diff --git a/src/View/GeoTIFF/List.py b/src/View/GeoTIFF/List.py
new file mode 100644
index 00000000..d604d72e
--- /dev/null
+++ b/src/View/GeoTIFF/List.py
@@ -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 .
+
+# -*- 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()
diff --git a/src/View/GeoTIFF/Translate.py b/src/View/GeoTIFF/Translate.py
new file mode 100644
index 00000000..3e328230
--- /dev/null
+++ b/src/View/GeoTIFF/Translate.py
@@ -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 .
+
+# -*- 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"
+ )
diff --git a/src/View/GeoTIFF/UndoCommand.py b/src/View/GeoTIFF/UndoCommand.py
new file mode 100644
index 00000000..9e26c1be
--- /dev/null
+++ b/src/View/GeoTIFF/UndoCommand.py
@@ -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 .
+
+# -*- 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()
diff --git a/src/View/GeoTIFF/Window.py b/src/View/GeoTIFF/Window.py
new file mode 100644
index 00000000..1fcf6650
--- /dev/null
+++ b/src/View/GeoTIFF/Window.py
@@ -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 .
+
+# -*- 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()
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 993b706a..d5ae0353 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -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:
diff --git a/src/View/MainWindowTabInfo.py b/src/View/MainWindowTabInfo.py
index 1dc959fb..a895f723 100644
--- a/src/View/MainWindowTabInfo.py
+++ b/src/View/MainWindowTabInfo.py
@@ -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):
diff --git a/src/View/PlotXY.py b/src/View/PlotXY.py
index c8ed228f..e51933ef 100644
--- a/src/View/PlotXY.py
+++ b/src/View/PlotXY.py
@@ -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()
diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py
index 88f5ebdf..a880c009 100644
--- a/src/View/Results/PlotXY.py
+++ b/src/View/Results/PlotXY.py
@@ -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
diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py
index ccac22ef..5a6277d2 100644
--- a/src/View/Results/Window.py
+++ b/src/View/Results/Window.py
@@ -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)
diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py
index 7a1e7e9b..95ddd9a4 100644
--- a/src/View/Results/translate.py
+++ b/src/View/Results/translate.py
@@ -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"
)
diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py
index 321c2864..ffa0e90f 100644
--- a/src/View/Tools/PamhyrWindow.py
+++ b/src/View/Tools/PamhyrWindow.py
@@ -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"
diff --git a/src/View/Translate.py b/src/View/Translate.py
index 30434d4f..e288e196 100644
--- a/src/View/Translate.py
+++ b/src/View/Translate.py
@@ -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")
diff --git a/src/View/WaitingDialog.py b/src/View/WaitingDialog.py
index 283fdc30..b61228e6 100644
--- a/src/View/WaitingDialog.py
+++ b/src/View/WaitingDialog.py
@@ -63,7 +63,7 @@ class WaitingDialog(PamhyrDialog):
". ", ". ",
".. ", ".. ",
"...", "..."],
- ["o ", " o ", " o ", " o", " o ", " o "],
+ ["o ", " o ", " o ", " o", " o ", " o "],
["█▓▒░", "▓█▓▒", "▒▓█▓", "░▒▓█", "▒▓█▓", "▓█▓▒"],
"▖▘▝▗",
"αβγδεζηθικλμνξοπρστυφχψω",
diff --git a/src/View/ui/GeoTIFF.ui b/src/View/ui/GeoTIFF.ui
new file mode 100644
index 00000000..48d953da
--- /dev/null
+++ b/src/View/ui/GeoTIFF.ui
@@ -0,0 +1,264 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 896
+ 504
+
+
+
+ MainWindow
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Cancel
+
+
+
+ -
+
+
+ Ok
+
+
+
+
+
+ -
+
+
+ Informations
+
+
+
-
+
+
+ Name
+
+
+
+ -
+
+
+ Enabled
+
+
+ true
+
+
+
+ -
+
+
+ Description
+
+
+
+ -
+
+
+ The relative file path on executable directory
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ GeoTIFF file
+
+
+
-
+
+
+ Import GeoTIFF file
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+
-
+
+
-
+
+
+ Reset
+
+
+
+ -
+
+
+ true
+
+
+ Right coordinate
+
+
+
+ -
+
+
+ Reset
+
+
+
+ -
+
+
+ 4
+
+
+ -99999999.000000000000000
+
+
+ 99999999.000000000000000
+
+
+
+ -
+
+
+ Reset
+
+
+
+ -
+
+
+ Reset
+
+
+
+ -
+
+
+ 4
+
+
+ -99999999.000000000000000
+
+
+ 99999999.000000000000000
+
+
+ 1.000000000000000
+
+
+
+ -
+
+
+ 4
+
+
+ -99999999.000000000000000
+
+
+ 99999999.000000000000000
+
+
+ 1.000000000000000
+
+
+
+ -
+
+
+ Top coordinate
+
+
+
+ -
+
+
+ 4
+
+
+ -99999999.000000000000000
+
+
+ 99999999.000000000000000
+
+
+
+ -
+
+
+ Left coordinate
+
+
+
+ -
+
+
+ Bottom coordinate
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/View/ui/GeoTIFFList.ui b/src/View/ui/GeoTIFFList.ui
new file mode 100644
index 00000000..b05ec968
--- /dev/null
+++ b/src/View/ui/GeoTIFFList.ui
@@ -0,0 +1,84 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 896
+ 504
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+
+ ressources/add.pngressources/add.png
+
+
+ Add
+
+
+ Add a new file
+
+
+
+
+
+ ressources/del.pngressources/del.png
+
+
+ Delete
+
+
+ Delete selected file(s)
+
+
+
+
+
+ ressources/edit.pngressources/edit.png
+
+
+ Edit
+
+
+ Edit file
+
+
+
+
+
+
diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui
index 96661c4f..b5c6f93f 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -129,6 +129,7 @@
&Geometry
+