Compare commits

...
51 Commits
Author SHA1 Message Date
Dylan Jeannin 5da607865c Pollutants: line becomes grey when we disable a pollutant + it's mentionned 'disabled' on the view only in the tab 2026-09-03 11:47:08 +02:00
Dylan Jeannin 1f5ce72fb2 Pollutants: fix deletion, didnt work correctly due to relative / flat indexes 2026-09-02 18:00:35 +02:00
Dylan Jeannin b6be245764 BoundaryConditionsAdisTS: display pollutant name in the edit window, to get a reminder of which pol we work on 2026-09-02 16:53:29 +02:00
Dylan Jeannin 46d0f52b3e Solver parameters: fix undo/redo command, double connection broke the normal operation of these commands 2026-09-02 16:20:03 +02:00
Dylan Jeannin 80f0b181d1 AdisTS: remove TEM files generated in results AdisTS (probably due to new solver version, including Temperature) 2026-09-02 14:33:31 +02:00
Dylan Jeannin 9161c54e18 AdisTS: debug results AdisTS with disabled profiles 2026-09-02 12:49:49 +02:00
Dylan Jeannin 64d9e456b7 Fix pep8 2026-09-01 18:13:02 +02:00
Dylan Jeannin 846aaeadf0 Translate: fix an error of translation 2026-09-01 17:57:39 +02:00
Dylan Jeannin eaf8d7364a InitialConditionsTemperature: fix temperature initial condition edit command 2026-09-01 17:53:30 +02:00
Dylan Jeannin 54bfff9623 Disable profile: dont check anymore if the profile we want to delete is attached to an initial condition 2026-09-01 17:47:03 +02:00
Dylan Jeannin 237364a4e2 Profiles: Fix plotting of empty geometry profiles, causing (non-blocking) error in the console 2026-09-01 16:57:50 +02:00
Dylan Jeannin c3288b44bb Meshing: Round interpolated profile RK values (2 decimal digits), to avoid XX.000000000001 values 2026-09-01 16:56:06 +02:00
Dylan Jeannin 70128bbdd1 Profiles: fix loaded profile reach reference (caused a crash during certain operations involving the profile table) 2026-09-01 16:31:33 +02:00
Dylan Jeannin a88a164ead GeometryProfilesXYZ: assignment of interpolated attribute, False by default, True when profile is created with mesh, and for the load of database, False by default, True when name starts with 'interpol' 2026-09-01 16:29:43 +02:00
Dylan Jeannin d6aeb5df21 Hydraulic structure: fix crashing bug happening when we select the section combobox without selecting a reach in the first combobox 2026-09-01 14:53:08 +02:00
Dylan Jeannin 32db5795da Hydraulic structures: Remove obsolete hydraulic structure output fields 2026-09-01 14:02:53 +02:00
Dylan Jeannin a81a641d41 Trad: Traduction of the new terms in french 2026-09-01 12:14:57 +02:00
Dylan Jeannin 53da35d959 Merge branch 'mesh_tab' into dev_dylan 2026-08-31 17:07:16 +02:00
Dylan Jeannin 0f1d1b0797 Time/Date: accept different formats for date, YYYY-MM-DD [HH:MM[:SS]], DD-MM-YYYY [HH:MM[:SS]] and DD/MM/YYYY [HH:MM[:SS]], convert it to classic one used by pamhyr, and prevent user when he's using a wrong one 2026-08-31 16:58:36 +02:00
Dylan Jeannin 6586a7b191 Date/time parameters: fixed the link between the start date in the study settings and the display in the tables 2026-08-31 16:14:28 +02:00
Dylan Jeannin b7aab9f263 Import geometry: debug case when we cannot open profiles after importing a new geometry 2026-08-31 14:42:45 +02:00
Dylan Jeannin ef34fecc31 Export geometry: doesnt export disabled profiles 2026-08-31 14:21:53 +02:00
Dylan Jeannin e9aa980c88 Export geometry: popup displayed before (and not after anymore) the file browser to choose if we export only geometry points, or if we want interpolated ones too 2026-08-31 14:08:09 +02:00
Dylan Jeannin ab495efc65 merge 2026-08-31 13:26:01 +02:00
Dylan Jeannin 14e13d6624 Moving points in network: selected node is now unselected after the end of moving, so clicking anywhere in the graph doesnt change his position anymore without refreshing the window 2026-08-31 11:57:33 +02:00
Dylan Jeannin 8db099c5c7 Moving points in network: selected node is now unselected after the end of moving, so clicking anywhere in the graph doesnt change his position anymore without refreshing the window 2026-08-31 11:56:01 +02:00
Dylan Jeannin b503e54210 Split reach: not displaying the first and the last reach in the combobox 2026-08-31 10:28:45 +02:00
Dylan Jeannin 68e4dfb598 Merge branch 'mesh_tab' into dev_dylan 2026-08-31 10:26:20 +02:00
JeaDylan 63dfab6654 Import INI files: non-blocking error if no profile associated with the RK is found in INI file, notifies the user of the relevant RK 2026-08-27 18:49:22 +02:00
JeaDylan 707577521f Import INI files: tolerate rounded RK values from INI files 2026-08-27 18:32:05 +02:00
JeaDylan 3458a5df9f Import INI files: handle INI RK matching and refresh table after import 2026-08-27 17:19:31 +02:00
JeaDylan e19eeedcc3 Export csv results: fix export flowrate as a function of time, on a study without sediments transport 2026-08-27 13:20:08 +02:00
JeaDylan c95de5e606 Merge branch 'temperature_test' into dev_dylan 2026-08-27 11:39:28 +02:00
Dylan Jeannin 53f7f380ae Result database: fix a problem that happens when we want to open a study with results saved, but results_table_data doesnt exist in database 2026-08-25 12:49:13 +02:00
Dylan Jeannin 2028d84554 Result database: fix a problem that happens when we want to open a study with results saved, but results_table_data doesnt exist in database 2026-08-25 12:48:02 +02:00
Dylan Jeannin da28bae6c5 Fix pep8 2026-08-24 17:08:37 +02:00
Dylan Jeannin 944f99f14b Disable profiles: security to prevent the disable of a profile associated with data (initial conditions, structures, lateral conditions, weather params, init. cond. temperature) 2026-08-24 17:01:16 +02:00
Dylan Jeannin 8978cc3482 Disabled profiles: the reach split is impossible on a disabled profile to avoid any problem 2026-08-24 15:37:03 +02:00
Dylan Jeannin f08d4b1a37 Disable profiles: results only take into account enabled profiles to display results for Mage 2026-08-24 14:17:39 +02:00
Dylan Jeannin 82b6cbfd72 Merge branch 'split_reach' into dev_dylan 2026-08-18 16:59:32 +02:00
Dylan Jeannin a44dddb51b Temperature results: display of temperature representation along the reach, with a scale of color 2026-08-13 13:00:49 +02:00
Dylan Jeannin 0fdb362313 Ovelapping intervals: setting default values when creating a parameter 2026-08-12 14:49:30 +02:00
Dylan Jeannin 50fa4f7bd8 Overlapping intervals: we cannot select rk if the interval overlaps anohter one, to avoid the confusion of multiple definitions over the same interval for WeatherParameters 2026-08-12 14:21:50 +02:00
Dylan Jeannin 9309aa9d2d Overlapping intervals: we cannot select rk that is already selected, to avoid the confusion of multiple definitions over the same interval 2026-08-12 14:03:17 +02:00
Dylan Jeannin dd31ffc5a2 Split reach: handling the case where the reach is split on a data used to define a data (an initial condition for example) 2026-08-12 12:09:47 +02:00
Dylan Jeannin 3d467c9620 Limit case: we cannot split reaches that contains less than 3 profiles, and not split on a limit profile (first or last one) 2026-08-11 17:32:15 +02:00
Dylan Jeannin f95c646ef3 Split reach: D90 and DIF now spread by the split 2026-08-11 17:32:06 +02:00
Dylan Jeannin c0000bb332 Database: upgrade version for temperature data 2026-08-11 11:47:34 +02:00
Dylan Jeannin 252ac257fe AdisTS AdisTT: fix initial conditions in scenario mode, now correctly saved in database, and load correctly, not creating duplicata when two datas have same ID with different scenario_id 2026-07-28 17:33:56 +02:00
Dylan Jeannin afe0217655 PEP8 2026-07-28 15:53:34 +02:00
Dylan Jeannin 516efb64e8 WeatherParam: SetData targets the object, and no longer the line index in the tab, which reduces the risk of error with multiple QUndoCommand, and rewrite of the logic for the other UndoCommand 2026-07-28 13:50:21 +02:00
72 changed files with 1896 additions and 741 deletions

No files matched your search

+4 -3
View File
@@ -137,10 +137,11 @@ class InternalMeshing(AMeshingTool):
p.points[k].z = pt1[k].z + \
dj*(pt2[k].z -
pt1[k].z)
p.rk = profiles[i].rk + \
dj*(profiles[i+1].rk -
profiles[i].rk)
p.rk = round(profiles[i].rk +
dj*(profiles[i+1].rk -
profiles[i].rk), 2)
p.name = f'interpol{p.rk}'
p.interpolated = True
new_profiles2.append(p)
new_profiles.append(new_profiles2)
@@ -77,7 +77,7 @@ class Data(SQLSubModel):
created = True
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute,
"boundary_condition_data_temperature"):
cls._db_create(execute)
@@ -205,7 +205,7 @@ class BoundaryConditionTemperature(SQLSubModel):
created = False
if major == "0" and (int(minor) < 2 or (
int(minor) == 2 and int(release) < 7)):
int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute,
"boundary_condition_temperature"):
cls._db_create(execute)
+41
View File
@@ -275,3 +275,44 @@ class D90AdisTS(SQLSubModel):
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
parts = [
(reach1.id, min(reach_rks), split_rk),
(reach2.id, split_rk, max(reach_rks)),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
+4
View File
@@ -61,6 +61,10 @@ class D90AdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for d90 in self.lst:
d90.split_reach(reach, profile, reach1, reach2)
@property
def D90_AdisTS_List(self):
return self.lst
+10
View File
@@ -261,3 +261,13 @@ class D90AdisTSSpec(SQLSubModel):
def enabled(self, enabled):
self._enabled = enabled
self.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = D90AdisTSSpec(name=self._name_section, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._d90 = self._d90
new._enabled = self._enabled
new.modified()
return new
+41
View File
@@ -329,3 +329,44 @@ class DIFAdisTS(SQLSubModel):
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
parts = [
(reach1.id, min(reach_rks), split_rk),
(reach2.id, split_rk, max(reach_rks)),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
+4
View File
@@ -61,6 +61,10 @@ class DIFAdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for dif in self.lst:
dif.split_reach(reach, profile, reach1, reach2)
@property
def DIF_AdisTS_List(self):
return self.lst
+12
View File
@@ -293,3 +293,15 @@ class DIFAdisTSSpec(SQLSubModel):
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = DIFAdisTSSpec(method=self._method, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._dif = self._dif
new._b = self._b
new._c = self._c
new._enabled = self._enabled
new.modified()
return new
+13 -7
View File
@@ -109,16 +109,22 @@ class FrictionList(PamhyrModelList):
((999 ** 100), -(999 ** 100))
)
frictions = list(
filter(
lambda f: ((rk_min <= f.begin_rk <= rk_max) or
(rk_min <= f.end_rk <= rk_max)),
self._lst))
for friction in self.lst:
lower, upper = sorted((friction.begin_rk, friction.end_rk))
clipped_lower = max(lower, rk_min)
clipped_upper = min(upper, rk_max)
if clipped_lower >= clipped_upper:
continue
for ind, friction in enumerate(frictions):
n = friction.cloned_for(reach)
if friction.begin_rk > friction.end_rk:
n.begin_rk = clipped_upper
n.end_rk = clipped_lower
else:
n.begin_rk = clipped_lower
n.end_rk = clipped_upper
new._lst.insert(ind, n)
new._lst.append(n)
new._status.modified()
return new
+42 -10
View File
@@ -59,7 +59,7 @@ class ProfileXYZ(Profile, SQLSubModel):
nb_point: int = 0,
code1: int = 0, code2: int = 0,
status=None, owner_scenario=-1,
enabled=True):
enabled=True, interpolated=False):
"""ProfileXYZ constructor
Args:
@@ -92,6 +92,7 @@ class ProfileXYZ(Profile, SQLSubModel):
self._station = []
self.station_up_to_date = False
self._enabled = bool(enabled)
self._interpolated = bool(interpolated)
self._get_water_limits_cache = {}
self._get_water_limits_ac_cache = {}
@@ -111,6 +112,7 @@ class ProfileXYZ(Profile, SQLSubModel):
code2 INTEGER NOT NULL,
sl INTEGER,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interpolated BOOLEAN NOT NULL DEFAULT FALSE,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
@@ -155,13 +157,27 @@ class ProfileXYZ(Profile, SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and int(minor) <=2:
if major == "0" and int(minor) <= 2:
if int(release) < 9:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and int(release) < 10)):
if minor != "0":
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN interpolated BOOLEAN NOT NULL DEFAULT FALSE"
)
execute(
"UPDATE geometry_profileXYZ " +
"SET interpolated = CASE " +
"WHEN LOWER(name) LIKE 'interpol%' THEN TRUE " +
"ELSE FALSE END"
)
return cls._update_submodel(execute, version, data)
@classmethod
@@ -247,7 +263,7 @@ class ProfileXYZ(Profile, SQLSubModel):
table = execute(
"SELECT pamhyr_id, ind, deleted, name, rk, num, " +
"code1, code2, sl, scenario, enabled " +
"code1, code2, sl, scenario, enabled, interpolated " +
"FROM geometry_profileXYZ " +
f"WHERE reach = {reach.id} " +
f"AND scenario = {scenario.id} " +
@@ -269,6 +285,7 @@ class ProfileXYZ(Profile, SQLSubModel):
sl = next(it)
owner_scenario = next(it)
enabled = (next(it) == 1)
interpolated = (next(it) == 1)
profile = cls(
id=pid, ind=ind, num=num,
@@ -277,7 +294,8 @@ class ProfileXYZ(Profile, SQLSubModel):
reach=reach,
status=status,
owner_scenario=owner_scenario,
enabled=enabled
enabled=enabled,
interpolated=interpolated
)
if deleted:
profile.set_as_deleted()
@@ -330,12 +348,14 @@ class ProfileXYZ(Profile, SQLSubModel):
execute(
"INSERT OR REPLACE INTO " +
"geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " +
"rk, num, code1, code2, sl, scenario, enabled) " +
"rk, num, code1, code2, sl, scenario, enabled, interpolated) " +
"VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self._name)}', " +
f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, {self._db_format(self._enabled)}" +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, " +
f"{self._db_format(self._enabled)}, " +
f"{self._db_format(self._interpolated)}" +
")"
)
@@ -403,7 +423,8 @@ class ProfileXYZ(Profile, SQLSubModel):
rk=self.rk,
reach=self.reach,
status=self._status,
enabled=self.is_enabled
enabled=self.is_enabled,
interpolated=self.interpolated
)
if self.is_deleted():
new_p.set_as_deleted()
@@ -424,7 +445,8 @@ class ProfileXYZ(Profile, SQLSubModel):
rk=self.rk,
reach=new_reach,
status=self._status,
enabled=self.is_enabled
enabled=self.is_enabled,
interpolated=self.interpolated
)
if self.is_deleted():
new_p.set_as_deleted()
@@ -981,7 +1003,7 @@ class ProfileXYZ(Profile, SQLSubModel):
Projection of the points of the profile on a plane.
"""
if self.nb_points < 2:
return [0.0]
return [0.0] * self.nb_points
else:
if self.station_up_to_date:
return self._station
@@ -1146,7 +1168,8 @@ class ProfileXYZ(Profile, SQLSubModel):
rk=self.rk,
reach=self.reach,
status=self._status,
enabled=self.is_enabled)
enabled=self.is_enabled,
interpolated=self.interpolated)
for i, k in enumerate(self.points):
p.insert_point(i, k.copy())
@@ -1193,3 +1216,12 @@ class ProfileXYZ(Profile, SQLSubModel):
def enabled(self, value):
self._enabled = bool(value)
self.modified()
@property
def interpolated(self):
return self._interpolated
@interpolated.setter
def interpolated(self, value):
self._interpolated = bool(value)
self.modified()
+36 -1
View File
@@ -80,11 +80,13 @@ class Reach(SQLSubModel):
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"], parent=data["reach"])
profile_data = data.copy()
profile_data["reach"] = new
new._profiles = sorted(
ProfileXYZ._db_load(
execute,
data=data.copy()
data=profile_data
),
key=lambda p: p._db_ind
)
@@ -375,6 +377,28 @@ class Reach(SQLSubModel):
)
)
def get_z_min_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_min(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_z_max_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_max(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk(self):
"""List of profiles rk
@@ -383,6 +407,17 @@ class Reach(SQLSubModel):
"""
return [profile.rk for profile in self.profiles]
def get_rk_enabled_profiles(self):
return list(
map(
lambda profile: profile.rk,
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk_complete_profiles(self):
return list(
map(
@@ -47,9 +47,7 @@ class HydraulicStructure(SQLSubModel):
self._name = name
self._input_section = None
self._output_section = None
self._input_reach = None
self._output_reach = None
self._enabled = True
self._data = []
@@ -62,17 +60,12 @@ class HydraulicStructure(SQLSubModel):
name TEXT NOT NULL,
enabled BOOLEAN NOT NULL,
input_reach INTEGER,
output_reach INTEGER,
input_section INTEGER,
output_section INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(input_reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(output_reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(input_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
FOREIGN KEY(output_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
@@ -92,13 +85,12 @@ class HydraulicStructure(SQLSubModel):
return True
if rl < 11:
for v in ["input", "output"]:
execute(
f"""
ALTER TABLE hydraulic_structures
RENAME COLUMN {v}_kp TO {v}_rk
"""
)
execute(
"""
ALTER TABLE hydraulic_structures
RENAME COLUMN input_kp TO input_rk
"""
)
cls._db_update_to_0_2_0(execute, data)
@@ -112,8 +104,28 @@ class HydraulicStructure(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and rl < 10)):
cls._db_update_to_0_2_10(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_10(cls, execute):
table = "hydraulic_structures"
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, deleted, name, enabled, input_section, " +
"input_reach, scenario) " +
"SELECT pamhyr_id, deleted, name, enabled, input_section, " +
f"input_reach, scenario FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "hydraulic_structures"
@@ -131,11 +143,10 @@ class HydraulicStructure(SQLSubModel):
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, enabled, input_section, output_section, " +
"input_reach, output_reach, scenario) " +
"(pamhyr_id, name, enabled, input_section, " +
"input_reach, scenario) " +
"SELECT pamhyr_id, name, enabled, " +
"input_section, output_section, " +
"input_reach, output_reach, scenario " +
"input_section, input_reach, scenario " +
f"FROM {table}"
)
@@ -145,44 +156,38 @@ class HydraulicStructure(SQLSubModel):
@classmethod
def _db_update_to_0_2_0_set_reach_pid(cls, execute, table, reachs):
els = execute(
f"SELECT pamhyr_id, input_reach, output_reach FROM {table}"
f"SELECT pamhyr_id, input_reach FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
in_reach_id = next(it)
out_reach_id = next(it)
if in_reach_id == -1:
return
if out_reach_id == -1:
out_reach_id = in_reach_id
execute(
f"UPDATE {table} " +
f"SET input_reach = {reachs[in_reach_id]}, " +
f"output_reach = {reachs[out_reach_id]} " +
f"SET input_reach = {reachs[in_reach_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_update_to_0_1_1(cls, execute, data,
origin_version="0.1.0"):
for v in ["input", "output"]:
execute(
"ALTER TABLE hydraulic_structures " +
f"ADD COLUMN {v}_section INTEGER"
)
execute(
"ALTER TABLE hydraulic_structures " +
"ADD COLUMN input_section INTEGER"
)
cls._db_update_to_0_1_1_assoc_section_from_rk(
execute, "hydraulic_structures",
reach_column=f"{v}_reach",
rk_column=f"{v}_rk",
section_column=f"{v}_section",
origin_version=origin_version
)
cls._db_update_to_0_1_1_assoc_section_from_rk(
execute, "hydraulic_structures",
reach_column="input_reach",
rk_column="input_rk",
section_column="input_section",
origin_version=origin_version
)
@classmethod
def _db_load(cls, execute, data=None):
@@ -196,8 +201,7 @@ class HydraulicStructure(SQLSubModel):
table = execute(
"SELECT pamhyr_id, deleted, name, enabled, " +
"input_section, output_section, " +
"input_reach, output_reach, scenario " +
"input_section, input_reach, scenario " +
"FROM hydraulic_structures " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
@@ -227,12 +231,7 @@ class HydraulicStructure(SQLSubModel):
input_section_id = (
-1 if input_section_id is None else input_section_id
)
output_section_id = next(it)
output_section_id = (
-1 if output_section_id is None else output_section_id
)
input_reach_id = next(it)
output_reach_id = next(it)
owner_scenario = next(it)
hs = cls(
@@ -243,27 +242,19 @@ class HydraulicStructure(SQLSubModel):
hs.set_as_deleted()
hs.enabled = enabled
hs.input_reach, hs.output_reach = reduce(
lambda acc, n: (
n if n.pamhyr_id == input_reach_id else acc[0],
n if n.pamhyr_id == output_reach_id else acc[1]
),
hs.input_reach = reduce(
lambda acc, n: n if n.pamhyr_id == input_reach_id else acc,
data["edges"],
[None, None]
None
)
sections = []
if hs.input_reach is not None:
sections += hs.input_reach.reach.profiles
if hs.output_reach is not None:
sections += hs.output_reach.reach.profiles
hs.input_section, hs.output_section = reduce(
lambda acc, s: (
s if s.pamhyr_id == input_section_id else acc[0],
s if s.pamhyr_id == output_section_id else acc[1]
),
hs.input_section = reduce(
lambda acc, s: s if s.pamhyr_id == input_section_id else acc,
sections,
[None, None]
None
)
loaded.add(hs_id)
@@ -287,24 +278,15 @@ class HydraulicStructure(SQLSubModel):
if self._input_reach is not None:
input_reach_id = self._input_reach.pamhyr_id
output_reach_id = -1
if self._output_reach is not None:
output_reach_id = self._output_reach.pamhyr_id
input_section = 'NULL'
if self.input_section is not None:
input_section = self.input_section.pamhyr_id
output_section = 'NULL'
if self.output_section is not None:
output_section = self.output_section.pamhyr_id
execute(
"INSERT INTO " +
"hydraulic_structures(" +
" pamhyr_id, deleted, name, enabled, " +
" input_section, output_section, " +
" input_reach, output_reach, " +
" input_section, input_reach, " +
" scenario" +
") " +
"VALUES (" +
@@ -312,8 +294,7 @@ class HydraulicStructure(SQLSubModel):
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"{self._db_format(self.enabled)}, " +
f"{input_section}, {output_section}, " +
f"{input_reach_id}, {output_reach_id}, " +
f"{input_section}, {input_reach_id}, " +
f"{self._status.scenario_id}"
")"
)
@@ -371,13 +352,6 @@ class HydraulicStructure(SQLSubModel):
return self._input_section
@property
def output_rk(self):
if self._output_section is None:
return None
return self._output_section
@property
def input_section(self):
return self._input_section
@@ -387,15 +361,6 @@ class HydraulicStructure(SQLSubModel):
self._input_section = input_section
self.modified()
@property
def output_section(self):
return self._output_section
@output_section.setter
def output_section(self, output_section):
self._output_section = output_section
self.modified()
@property
def enabled(self):
return self._enabled
@@ -414,27 +379,15 @@ class HydraulicStructure(SQLSubModel):
self._input_reach = input_reach
self.modified()
@property
def output_reach(self):
return self._output_reach
@output_reach.setter
def output_reach(self, output_reach):
self._output_reach = output_reach
self.modified()
@property
def basic_structures(self):
return self.lst.copy()
def cloned_for(self, input_reach, input_section,
output_reach, output_section):
def cloned_for(self, input_reach, input_section):
new = HydraulicStructure(name=self._name, status=self._status)
new._enabled = self._enabled
new._input_reach = input_reach
new._input_section = input_section
new._output_reach = output_reach
new._output_section = output_section
new._data = [structure.cloned() for structure in self.lst]
new.modified()
return new
@@ -87,8 +87,7 @@ class HydraulicStructureList(PamhyrModelList):
structures = [
structure
for structure in self.lst
if (structure.input_reach is reach or
structure.output_reach is reach)
if structure.input_reach is reach
]
for structure in structures:
@@ -96,15 +95,9 @@ class HydraulicStructureList(PamhyrModelList):
structure.input_reach,
structure.input_section
)
output_reach, output_section = split_endpoint(
structure.output_reach,
structure.output_section
)
self._lst.append(structure.cloned_for(
input_reach,
input_section,
output_reach,
output_section
input_section
))
if len(structures) != 0:
@@ -19,6 +19,7 @@
import logging
from copy import copy, deepcopy
from math import isclose
from tools import trace, timer
from functools import reduce
from numpy import interp
@@ -439,15 +440,28 @@ class InitialConditions(SQLSubModel):
def new_from_data(self, rk, discharge, elevation):
n = Data(reach=self._reach, status=self._status)
# Values read from Mage INI files are strings, whereas profile RK
# values are stored as floats. Normalize the imported value before
# looking up its associated profile.
rk = float(rk)
section = reduce(
lambda acc, s: (
s if s.rk == rk else acc
# Mage INI files store RK values with two decimal places.
# Allow for the rounding introduced by that format.
s if isclose(s.rk, rk, rel_tol=0.0, abs_tol=0.0051)
else acc
),
self._reach.reach.profiles,
None
)
if section is None:
raise ValueError(
f"No profile with RK {rk} exists in reach "
f"{self._reach.name}"
)
n['section'] = section
n['discharge'] = discharge
n['elevation'] = elevation
@@ -391,7 +391,7 @@ class InitialConditionsAdisTS(SQLSubModel):
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
@@ -192,7 +192,7 @@ class ICAdisTSSpec(SQLSubModel):
new_spec.rate = rate
new_spec.enabled = enabled
# loaded.add(pid)
loaded.add(id)
new.append(new_spec)
data["scenario"] = scenario.parent
@@ -237,6 +237,7 @@ class ICAdisTSSpec(SQLSubModel):
def name(self, name):
self._name_section = name
self.modified()
self.modified()
@property
def reach(self):
@@ -246,6 +247,7 @@ class ICAdisTSSpec(SQLSubModel):
def reach(self, reach):
self._reach = reach
self.modified()
self.modified()
@property
def start_rk(self):
@@ -255,6 +257,7 @@ class ICAdisTSSpec(SQLSubModel):
def start_rk(self, start_rk):
self._start_rk = start_rk
self.modified()
self.modified()
@property
def end_rk(self):
@@ -264,6 +267,7 @@ class ICAdisTSSpec(SQLSubModel):
def end_rk(self, end_rk):
self._end_rk = end_rk
self.modified()
self.modified()
@property
def concentration(self):
@@ -273,6 +277,7 @@ class ICAdisTSSpec(SQLSubModel):
def concentration(self, concentration):
self._concentration = concentration
self.modified()
self.modified()
@property
def eg(self):
@@ -282,6 +287,7 @@ class ICAdisTSSpec(SQLSubModel):
def eg(self, eg):
self._eg = eg
self.modified()
self.modified()
@property
def em(self):
@@ -291,6 +297,7 @@ class ICAdisTSSpec(SQLSubModel):
def em(self, em):
self._em = em
self.modified()
self.modified()
@property
def ed(self):
@@ -300,6 +307,7 @@ class ICAdisTSSpec(SQLSubModel):
def ed(self, ed):
self._ed = ed
self.modified()
self.modified()
@property
def rate(self):
@@ -309,6 +317,7 @@ class ICAdisTSSpec(SQLSubModel):
def rate(self, rate):
self._rate = rate
self.modified()
self.modified()
@property
def enabled(self):
@@ -68,7 +68,7 @@ class InitialConditionsTemperature(SQLSubModel):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) < 7):
if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7):
table_name = "initial_conditions_temperature"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
@@ -167,7 +167,11 @@ class InitialConditionsTemperature(SQLSubModel):
return True
def __len__(self):
return len(self._data)
return len(self.data)
@property
def data(self):
return list(filter(lambda item: not item.is_deleted(), self._data))
@property
def name(self):
@@ -254,7 +258,7 @@ class InitialConditionsTemperature(SQLSubModel):
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
@@ -31,10 +31,11 @@ class ICTemperatureSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=None):
super(ICTemperatureSpec, self).__init__()
self._status = status
status=None, owner_scenario=-1):
super(ICTemperatureSpec, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name_section = name
self._reach = None
@@ -69,7 +70,7 @@ class ICTemperatureSpec(SQLSubModel):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) < 7):
if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7):
table_name = "initial_conditions_temperature_spec"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
@@ -122,7 +123,7 @@ class ICTemperatureSpec(SQLSubModel):
new_spec.end_rk = end_rk
new_spec.temperature = temperature
# loaded.add(pid)
loaded.add(id)
new.append(new_spec)
data["scenario"] = scenario.parent
@@ -163,7 +164,7 @@ class ICTemperatureSpec(SQLSubModel):
@name.setter
def name(self, name):
self._name_section = name
self._status.modified()
self.modified()
@property
def reach(self):
@@ -172,7 +173,7 @@ class ICTemperatureSpec(SQLSubModel):
@reach.setter
def reach(self, reach):
self._reach = reach
self._status.modified()
self.modified()
@property
def start_rk(self):
@@ -181,7 +182,7 @@ class ICTemperatureSpec(SQLSubModel):
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self._status.modified()
self.modified()
@property
def end_rk(self):
@@ -190,7 +191,7 @@ class ICTemperatureSpec(SQLSubModel):
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self._status.modified()
self.modified()
@property
def temperature(self):
@@ -117,7 +117,7 @@ class LateralContributionList(PamhyrModelListWithTab):
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
if clipped_lower >= clipped_upper:
continue
begin_index = (
@@ -95,7 +95,7 @@ class LateralContributionsAdisTSList(PamhyrModelList):
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
if clipped_lower >= clipped_upper:
continue
begin_rk = clipped_upper if reverse else clipped_lower
+3 -8
View File
@@ -202,13 +202,8 @@ class TableData(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:
if int(release) < 7 and not create:
cls._db_create(execute)
create = True
if not cls.is_table_exists(execute, "results_table_data"):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@@ -527,7 +522,7 @@ class Results(SQLSubModel):
i = 0
for reach in new_results._river.reachs:
nb = len(reach.profiles)
reach.set_global_index(range(i, i + nb + 1))
reach.set_global_index(range(i, i + nb))
i += nb
if "Z" in table_data:
+1
View File
@@ -154,6 +154,7 @@ class Profile(SQLSubModel):
"len_data, data, scenario " +
"FROM results_data " +
f"WHERE scenario = {scenario.id} " +
f"AND result = {data['result_pid']} " +
f"AND reach = {reach.pamhyr_id} " +
f"AND section = {profile.pamhyr_id}"
)
+10 -4
View File
@@ -64,13 +64,15 @@ class Profile(object):
class Reach(object):
def __init__(self, reach, study):
def __init__(self, reach, study, profiles=None):
self._study = study
self._reach = reach # Source reach in the study
if profiles is None:
profiles = reach.profiles
self._profiles = list(
map(
lambda p: Profile(p, self._study),
reach.profiles
profiles
)
)
@@ -89,6 +91,10 @@ class Reach(object):
def profiles(self):
return self._profiles.copy()
def get_rk(self):
"""Return the PK values for profiles present in the results."""
return [profile.rk for profile in self._profiles]
def profile(self, id):
return self._profiles[id]
@@ -119,10 +125,10 @@ class River(object):
def has_reach(self, id):
return 0 <= id < len(self._reachs)
def add(self, reach_id):
def add(self, reach_id, profiles=None):
reachs = self._study.river.enable_edges()
new = Reach(reachs[reach_id].reach, self._study)
new = Reach(reachs[reach_id].reach, self._study, profiles=profiles)
self._reachs.append(new)
return new
+61
View File
@@ -907,6 +907,55 @@ Last export at: @date."""
def weather_parameters(self):
return self._WeatherParameters
def profile_dependencies(self, profile):
"""Return model data which directly reference *profile*.
PK-based intervals are deliberately not included: only explicit
references to the profile object prevent it from being disabled.
"""
dependencies = []
def add(kind, item, association):
dependencies.append({
"kind": kind,
"item": item,
"association": association,
})
for structure in self._hydraulic_structures.lst:
if structure.input_section is profile:
add("hydraulic_structure", structure, "")
for tab in self._lateral_contribution._tabs_list:
for contribution in self._lateral_contribution.get_tab(tab):
if contribution.begin_section is profile:
add("lateral_contribution", contribution, "begin_section")
if contribution.end_section is profile:
add("lateral_contribution", contribution, "end_section")
for parameter in self._WeatherParameters.lst:
if parameter.begin_section is profile:
add("weather_parameter", parameter, "begin_section")
if parameter.end_section is profile:
add("weather_parameter", parameter, "end_section")
reach_id = profile.reach.pamhyr_id
for initial_condition in self._InitialConditionsTemperature.lst:
for specification in initial_condition.data:
same_reach = specification.reach == reach_id
same_rk = (
specification.start_rk == profile.rk or
specification.end_rk == profile.rk
)
if same_reach and same_rk:
add(
"initial_condition_temperature",
specification,
"cross_section_pk",
)
return dependencies
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]
@@ -963,6 +1012,12 @@ Last export at: @date."""
self._results[solv_type] = results
def _split_reach(self, reach, profile):
profiles = reach.reach.profiles
if not profiles or profile not in profiles[1:-1]:
raise ValueError(
"A reach cannot be split at its first or last profile"
)
node1 = reach.node1
node2 = reach.node2
@@ -1000,5 +1055,11 @@ Last export at: @date."""
self._WeatherParameters.split_reach(
reach, profile, r1, r2
)
self._D90AdisTS.split_reach(
reach, profile, r1, r2
)
self._DIFAdisTS.split_reach(
reach, profile, r1, r2
)
return r1, r2
+10 -3
View File
@@ -46,7 +46,7 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.9"
_version = "0.2.10"
_sub_classes = [
Scenario,
@@ -290,7 +290,14 @@ class Study(SQLModel):
if major == "0" and int(minor) < 2:
self._add_into_info_if_not_exists('current_scenario', '0')
if major == "0" and int(minor) < 2:
needs_foreign_keys_disabled = (
major == "0" and (
int(minor) < 2 or
(minor == "2" and int(release) < 10)
)
)
if needs_foreign_keys_disabled:
# Need to temporary disable the sqlite foreign keys
# checking to update db dans change the table id fk to
# table pamhyr_id fk
@@ -300,7 +307,7 @@ class Study(SQLModel):
ok = self._update_submodel(version[0], data={})
if major == "0" and int(minor) < 2:
if needs_foreign_keys_disabled:
# Reactivate foreign keys checking
self.execute(
"PRAGMA foreign_keys = ON;"
@@ -73,7 +73,7 @@ class Data(SQLSubModel):
major, minor, release = version.strip().split(".")
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute, "air_temperature_data"):
cls._db_create(execute)
@@ -341,7 +341,7 @@ class WeatherParameters(SQLSubModel):
created = False
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute, "air_temperature"):
cls._db_create(execute)
created = True
@@ -652,14 +652,38 @@ class WeatherParameters(SQLSubModel):
def add(self, index: int):
value = Data(self._default_0, self._default_1, status=self._status)
self._data.insert(index, value)
self.modified()
self.insert(index, value)
return value
def insert(self, index: int, value):
visible_data = self.data
if index < len(visible_data):
index = self._data.index(visible_data[index])
else:
index = len(self._data)
self._data.insert(index, value)
self.modified()
def set_data_value(self, value, column, data):
value[column] = self._types[column](data)
self.modified()
def set_deleted(self, values, deleted):
for value in values:
if deleted:
value.set_as_deleted()
else:
value.set_as_not_deleted()
self.modified()
def reorder(self, values):
values = iter(values)
self._data = [
value if value.is_deleted() else next(values)
for value in self._data
]
self.modified()
def delete_i(self, indexes):
self._data = list(
map(
@@ -708,10 +732,7 @@ class WeatherParameters(SQLSubModel):
return lst
def _set_i_c_v(self, index, column, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
self.set_data_value(self.get_i(index), column, value)
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
@@ -742,15 +763,13 @@ class WeatherParameters(SQLSubModel):
return new
def move_up(self, index):
if index < len(self):
next = index - 1
d = self._data
d[index], d[next] = d[next], d[index]
self.modified()
if 0 < index < len(self):
data = self.data
data[index - 1], data[index] = data[index], data[index - 1]
self.reorder(data)
def move_down(self, index):
if index >= 0:
prev = index + 1
d = self._data
d[index], d[prev] = d[prev], d[index]
self.modified()
if 0 <= index < len(self) - 1:
data = self.data
data[index], data[index + 1] = data[index + 1], data[index]
self.reorder(data)
@@ -204,7 +204,7 @@ class WeatherParametersList(PamhyrModelList):
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
if clipped_lower >= clipped_upper:
continue
begin_index = clipped_upper if reverse else clipped_lower
+35
View File
@@ -408,6 +408,41 @@ class AdisTS(CommandLineSolver):
# RESULTS #
###########
@staticmethod
def _remove_temperature_results(repertory):
"""Remove temperature outputs generated but unused by AdisTS."""
results_directory = os.path.join(repertory, "resultats")
if not os.path.isdir(results_directory):
results_directory = repertory
if not os.path.isdir(results_directory):
return
for filename in os.listdir(results_directory):
if not (filename.startswith("TEM.") or
filename.startswith("TEM_")):
continue
filepath = os.path.join(results_directory, filename)
if os.path.isfile(filepath):
try:
os.remove(filepath)
logger.debug(
f"Removed unused AdisTS result '{filepath}'"
)
except OSError as error:
logger.warning(
f"Unable to remove AdisTS result '{filepath}': "
f"{error}"
)
def run_output_data_fomater(self, study):
# AdisTS generates temperature results that are not used by the solver,
# but they are still present in the output directory.
# Remove them to avoid confusion.
self._remove_temperature_results(self._process.workingDirectory())
return super(AdisTS, self).run_output_data_fomater(study)
def read_bin(self, study, repertory, results, qlog=None, name="0"):
return
+8 -2
View File
@@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -97,7 +97,9 @@ class TableModel(PamhyrTableModel):
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -114,6 +116,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
+2 -1
View File
@@ -173,7 +173,8 @@ class EditBoundaryConditionWindow(PamhyrWindow):
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
opt_data=self._study.time_system,
start_date=self._study.date
)
table.setModel(self._table)
@@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -87,7 +87,9 @@ class TableModel(PamhyrTableModel):
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -104,6 +106,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
@@ -64,14 +64,22 @@ class EditBoundaryConditionWindow(PamhyrWindow):
name = trad[self._pamhyr_name]
if self._data is not None:
pollutant_name = next(
(
pollutant.name
for pollutant in study.river.Pollutants.Pollutants_List
if pollutant.id == self._data.pollutant
),
None
)
n = self._data.node
node_name = next(filter(
lambda x: x.id == n, study.river._nodes
)).name
name += (
f" - {study.name} " +
f"({node_name})"
)
name += f" - {study.name}"
if pollutant_name is not None:
name += f" - {pollutant_name}"
name += f" ({node_name})"
super(EditBoundaryConditionWindow, self).__init__(
title=name,
@@ -120,7 +128,8 @@ class EditBoundaryConditionWindow(PamhyrWindow):
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
opt_data=self._study.time_system,
start_date=self._study.date
)
table.setModel(self._table)
@@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -87,7 +87,9 @@ class TableModel(PamhyrTableModel):
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -104,6 +106,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
@@ -117,7 +117,8 @@ class EditBoundaryConditionWindow(PamhyrWindow):
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
opt_data=self._study.time_system,
start_date=self._study.date
)
table.setModel(self._table)
+8 -1
View File
@@ -123,9 +123,16 @@ class D90TableModel(PamhyrTableModel):
self._undo.indexChanged.connect(lambda _: self.update())
def _setup_lst(self):
enabled_reaches = {
reach.id for reach in self._river.enable_edges()
}
self._lst = list(
filter(
lambda d90: d90._deleted is False,
lambda d90: (
d90._deleted is False
and (d90.reach in (None, -1)
or d90.reach in enabled_reaches)
),
self._data._data
)
)
+8 -1
View File
@@ -125,9 +125,16 @@ class DIFTableModel(PamhyrTableModel):
self._undo.indexChanged.connect(lambda _: self.update())
def _setup_lst(self):
enabled_reaches = {
reach.id for reach in self._river.enable_edges()
}
self._lst = list(
filter(
lambda dif: dif._deleted is False,
lambda dif: (
dif._deleted is False
and (dif.reach in (None, -1)
or dif.reach in enabled_reaches)
),
self._data._data
)
)
+1 -2
View File
@@ -171,7 +171,7 @@ class ProfileWindow(PamhyrWindow):
def profiles(self):
"""Return profiles from the geometry model owning this profile."""
return self._profile.reach.reach.profiles
return self._profile.reach.profiles
def navigate_profile(self, offset):
profiles = self.profiles()
@@ -204,7 +204,6 @@ class ProfileWindow(PamhyrWindow):
self._tablemodel.beginResetModel()
self._tablemodel._data = profile
# update lst to display correct data and apply connexions to add/remove points
self._tablemodel._setup_lst()
self._tablemodel.endResetModel()
+54 -2
View File
@@ -19,6 +19,7 @@
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
from View.WeatherParameters.translate import WeatherParametersTranslate
_translate = QCoreApplication.translate
@@ -47,6 +48,57 @@ class GeometryTranslate(MainTranslate):
self._dict["cross_section_enabled_count"] = _translate(
"Geometry", "Cross-section enabled ({count} disabled)"
)
self._dict["profile_disable_blocked"] = _translate(
"Geometry", "The cross-section cannot be disabled."
)
self._dict["profile_disable_blocked_info"] = _translate(
"Geometry",
"It is referenced by the following data:\n{dependencies}\n\n"
"Change or remove these associations before disabling it."
)
self._dict["profile_dependency"] = _translate(
"Geometry", "{kind} “{name}” ({association})"
)
self._dict["initial_condition"] = _translate(
"Geometry", "Initial condition"
)
self._dict["initial_condition_temperature"] = _translate(
"Geometry", "Initial condition temperature"
)
self._dict["hydraulic_structure"] = _translate(
"Geometry", "Hydraulic structure"
)
self._dict["lateral_contribution"] = _translate(
"Geometry", "Lateral contribution"
)
self._dict["weather_parameter"] = _translate(
"Geometry", "Weather parameter"
)
self._dict["section"] = _translate("Geometry", "cross-section")
self._dict["begin_section"] = _translate(
"Geometry", "beginning cross-section"
)
self._dict["end_section"] = _translate(
"Geometry", "end cross-section"
)
self._dict["cross_section_pk"] = _translate(
"Geometry", "cross-section PK"
)
weather_parameters = WeatherParametersTranslate().get_dict(
"weather_parameters"
)
self._sub_dict["weather_parameter_types"] = {
"AT": weather_parameters["air_temperature"],
"SH": weather_parameters["specific_humidity"],
"GR": weather_parameters["global_radiation"],
"RWS": weather_parameters["reference_wind_speed"],
"GT": weather_parameters["surface_temperature"],
"GFR": weather_parameters["surface_flow_rate"],
"ALB": weather_parameters["albedo"],
"SC": weather_parameters["shading_coefficient"],
"CCF": weather_parameters["cloud_cover_fraction"],
}
self._dict["transverse_abscissa"] = _translate(
"Geometry", "Transverse abscissa (m)"
@@ -104,10 +156,10 @@ class GeometryTranslate(MainTranslate):
"Geometry", "Export options"
)
self._dict["export_options_text"] = _translate(
"Geometry", "Choose which cross-sections to export."
"Geometry", "Exportation of the cross-sections."
)
self._dict["export_interpolated_profiles"] = _translate(
"Geometry", "Export interpolated cross-sections"
"Geometry", "Export with interpolated cross-sections"
)
self._dict["update_rk_error"] = _translate(
"Geometry",
+69 -11
View File
@@ -103,6 +103,7 @@ class GeometryWindow(PamhyrWindow):
self.setup_plots()
self.setup_statusbar()
self.setup_connections()
self.update_meshing_action()
def setup_table(self):
if self._study.is_read_only():
@@ -239,7 +240,16 @@ class GeometryWindow(PamhyrWindow):
def update_redraw(self):
self._update(redraw=True)
def update_meshing_action(self):
enabled = (
not self._study.is_read_only()
and self._reach.number_profiles > 0
)
self.find(QAction, "action_meshing").setEnabled(enabled)
def _update(self, redraw=False, propagate=True):
self.update_meshing_action()
if redraw:
self._plot_xy.redraw(data=self._reach)
self._plot_rkc.redraw(data=self._reach)
@@ -311,6 +321,9 @@ class GeometryWindow(PamhyrWindow):
self.tableView.model().blockSignals(False)
def edit_meshing(self):
if self._reach.number_profiles == 0:
return
rows = list(
set(
(i.row() for i in self.tableView.selectedIndexes())
@@ -544,7 +557,8 @@ class GeometryWindow(PamhyrWindow):
def _set_checkbox_state(self):
rows = self.index_selected_rows()
editable = not self._study.is_read_only()
disabled_count = self._reach.number_profiles - self._reach.number_enabled_profiles
disabled_count = (self._reach.number_profiles -
self._reach.number_enabled_profiles)
self._checkbox.setText(
self._trad["cross_section_enabled_count"].format(
count=disabled_count
@@ -573,6 +587,49 @@ class GeometryWindow(PamhyrWindow):
if len(rows) == 0:
return
if not enabled:
blocked = []
for row in rows:
profile = self._reach.profile(row)
dependencies = self._study.river.profile_dependencies(profile)
if len(dependencies) != 0:
blocked.append((profile, dependencies))
if len(blocked) != 0:
details = []
for profile, dependencies in blocked:
details.append(f"{profile.name}:")
for dependency in dependencies:
item = dependency["item"]
if dependency["kind"] == "weather_parameter":
name = self._trad.get_dict(
"weather_parameter_types"
).get(item.type, item.type)
else:
name = getattr(item, "name", "")
if name == "":
name = self._trad[dependency["kind"]]
details.append(
" - " +
self._trad["profile_dependency"].format(
kind=self._trad[dependency["kind"]],
name=name,
association=self._trad[
dependency["association"]
],
)
)
self._set_checkbox_state()
self.message_box(
self._trad["profile_disable_blocked"],
self._trad["profile_disable_blocked_info"].format(
dependencies="\n".join(details)
),
window_title=self._trad["warning"],
)
return
self._checkbox.setTristate(False)
self._table.enabled(rows, enabled)
@@ -761,6 +818,10 @@ class GeometryWindow(PamhyrWindow):
# self.update_plot_rkc()
def export_to_file(self):
export_interpolated = self._ask_export_options()
if export_interpolated is None:
return
settings = QSettings(
QSettings.IniFormat,
QSettings.UserScope, 'MyOrg'
@@ -790,11 +851,9 @@ class GeometryWindow(PamhyrWindow):
suffix = Path(filename).suffix
if suffix != "":
if suffix == ".st" or suffix == ".ST":
export_interpolated = self._ask_export_options()
if export_interpolated is not None:
self._export_to_file_st(
filename[:-3], export_interpolated
)
self._export_to_file_st(
filename[:-3], export_interpolated
)
else:
# Warning popup when the format is not exportable
win = QtWidgets.QMessageBox()
@@ -805,9 +864,7 @@ class GeometryWindow(PamhyrWindow):
pass
else:
export_interpolated = self._ask_export_options()
if export_interpolated is not None:
self._export_to_file_st(filename, export_interpolated)
self._export_to_file_st(filename, export_interpolated)
def _ask_export_options(self):
win = QtWidgets.QMessageBox(self)
@@ -844,8 +901,9 @@ class GeometryWindow(PamhyrWindow):
if is_interpolated and not export_interpolated:
continue
self._export_to_file_st_profile(wfile, profile, pid)
pid += 1
if profile.enabled:
self._export_to_file_st_profile(wfile, profile, pid)
pid += 1
def _export_to_file_st_profile(self, wfile, profile, pid):
num = f"{pid:>6}"
+9 -6
View File
@@ -96,14 +96,17 @@ class ComboBoxDelegate(QItemDelegate):
reach = self._data.hydraulic_structures\
.get_for_enabled_reaches(index.row())\
.input_reach
profiles = list(
filter(
lambda p: p.display_name() == text,
reach.reach.profiles
if reach is None:
value = None
else:
profiles = list(
filter(
lambda p: p.display_name() == text,
reach.reach.profiles
)
)
)
value = profiles[0] if len(profiles) > 0 else None
value = profiles[0] if len(profiles) > 0 else None
else:
value = text
+29 -21
View File
@@ -31,7 +31,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
QComboBox, QMessageBox
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -290,8 +290,6 @@ class InitialConditionTableModel(PamhyrTableModel):
logger.error("No results data")
return
self.layoutAboutToBeChanged.emit()
ts = max(results.get("timestamps"))
res_reach = results.river.get_reach_by_geometry(
self._reach.reach
@@ -307,19 +305,15 @@ class InitialConditionTableModel(PamhyrTableModel):
)
)
self._undo.push(
ReplaceDataCommand(
self._lst,
list(
map(
lambda d: self._lst.new_from_data(*d),
data
)
)
new_data = list(
map(
lambda d: self._lst.new_from_data(*d),
data
)
)
self.layoutAboutToBeChanged.emit()
self._undo.push(ReplaceDataCommand(self._lst, new_data))
self.layoutChanged.emit()
def read_from_ini(self, file_name):
@@ -355,17 +349,31 @@ class InitialConditionTableModel(PamhyrTableModel):
line_split[2],
line_split[3]])
self._undo.push(
ReplaceDataCommand(
self._lst,
list(
map(
lambda d: self._lst.new_from_data(*d),
data
)
new_data = []
missing_rks = []
for row in data:
rk = row[0].strip()
try:
new_data.append(self._lst.new_from_data(*row))
except ValueError:
missing_rks.append(rk)
logger.warning(
f"No profile found for imported RK {rk} "
f"in reach {self._reach.name}"
)
self.layoutAboutToBeChanged.emit()
self._undo.push(ReplaceDataCommand(self._lst, new_data))
self.layoutChanged.emit()
if missing_rks:
QMessageBox.warning(
self._table_view,
self._trad["missing_rk_title"],
self._trad["missing_rk_text"].format(
rks=", ".join(missing_rks)
)
)
)
def undo(self):
self._undo.undo()
+7
View File
@@ -42,6 +42,13 @@ class ICTranslate(MainTranslate):
"InitialCondition", "Mage initial conditions file (*.INI *.ini)")
self._dict["file_all"] = _translate(
"InitialCondition", "All files (*)")
self._dict["missing_rk_title"] = _translate(
"InitialCondition", "Profiles not found")
self._dict["missing_rk_text"] = _translate(
"InitialCondition",
"No profile was found for the following RK values: {rks}. "
"They were not imported and must be entered manually."
)
self._sub_dict["table_headers"] = {
# "name": _translate("InitialCondition", "Name"),
+3 -2
View File
@@ -205,13 +205,14 @@ class InitialConditionTableModel(PamhyrTableModel):
if self._headers[column] != "reach":
self._undo.push(
SetCommandSpec(
self._lst, row, self._headers[column], value
self._data, self._lst, row,
self._headers[column], value
)
)
elif self._headers[column] == "reach":
self._undo.push(
SetCommandSpec(
self._lst, row, self._headers[column],
self._data, self._lst, row, self._headers[column],
self._river.edge(value).id
)
)
+16 -48
View File
@@ -80,31 +80,31 @@ class SetCommand(QUndoCommand):
class SetCommandSpec(QUndoCommand):
def __init__(self, data, row, column, new_value):
def __init__(self, parent, data, row, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._parent = parent
self._value = data[row]
self._column = column
if self._column == "name":
self._old = self._data[self._row].name
self._old = self._value.name
elif self._column == "reach":
self._old = self._data[self._row].reach
self._old = self._value.reach
elif self._column == "start_rk":
self._old = self._data[self._row].start_rk
self._old = self._value.start_rk
elif self._column == "end_rk":
self._old = self._data[self._row].end_rk
self._old = self._value.end_rk
elif self._column == "concentration":
self._old = self._data[self._row].concentration
self._old = self._value.concentration
elif self._column == "eg":
self._old = self._data[self._row].eg
self._old = self._value.eg
elif self._column == "em":
self._old = self._data[self._row].em
self._old = self._value.em
elif self._column == "ed":
self._old = self._data[self._row].ed
self._old = self._value.ed
elif self._column == "rate":
self._old = self._data[self._row].rate
self._old = self._value.rate
_type = float
if column == "name":
@@ -115,44 +115,12 @@ class SetCommandSpec(QUndoCommand):
self._new = _type(new_value)
def undo(self):
if self._column == "name":
self._data[self._row].name = self._old
elif self._column == "reach":
self._data[self._row].reach = self._old
elif self._column == "start_rk":
self._data[self._row].start_rk = self._old
elif self._column == "end_rk":
self._data[self._row].end_rk = self._old
elif self._column == "concentration":
self._data[self._row].concentration = self._old
elif self._column == "eg":
self._data[self._row].eg = self._old
elif self._column == "em":
self._data[self._row].em = self._old
elif self._column == "ed":
self._data[self._row].ed = self._old
elif self._column == "rate":
self._data[self._row].rate = self._old
setattr(self._value, self._column, self._old)
self._parent.modified()
def redo(self):
if self._column == "name":
self._data[self._row].name = self._new
elif self._column == "reach":
self._data[self._row].reach = self._new
elif self._column == "start_rk":
self._data[self._row].start_rk = self._new
elif self._column == "end_rk":
self._data[self._row].end_rk = self._new
elif self._column == "concentration":
self._data[self._row].concentration = self._new
elif self._column == "eg":
self._data[self._row].eg = self._new
elif self._column == "em":
self._data[self._row].em = self._new
elif self._column == "ed":
self._data[self._row].ed = self._new
elif self._column == "rate":
self._data[self._row].rate = self._new
setattr(self._value, self._column, self._new)
self._parent.modified()
class AddCommand(QUndoCommand):
+37 -8
View File
@@ -30,13 +30,13 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
QComboBox, QMessageBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.InitialConditionsTemperature.UndoCommand import (
SetCommand, AddCommand, SetCommandSpec,
AddCommand, SetCommandSpec,
DelCommand,
)
@@ -229,13 +229,22 @@ class InitialConditionTableModel(PamhyrTableModel):
row = index.row()
column = index.column()
column_name = self._headers[column]
try:
if self._headers[column] in ["name", "temperature"]:
self._undo.push(
SetCommand(
self._lst, row, self._headers[column], value
)
new_value = (
self._river.edge(value).id
if column_name == "reach"
else value
)
if (column_name in ("reach", "rk")
and self._overlaps_existing_rk(
row, column_name, new_value
)):
QMessageBox.warning(
self._table_view,
self._trad["msg_rk_overlap_title"],
self._trad["msg_rk_overlap_text"]
)
else:
new_value = value
@@ -248,7 +257,8 @@ class InitialConditionTableModel(PamhyrTableModel):
self._undo.push(
SetCommandSpec(
self._lst, row, self._headers[column],
self._data, self._lst, row,
self._headers[column],
new_value
)
)
@@ -265,6 +275,25 @@ class InitialConditionTableModel(PamhyrTableModel):
self.dataChanged.emit(index, index)
return True
def _overlaps_existing_rk(self, row, column, value):
current = self._lst[row]
reach = value if column == "reach" else current.reach
rk = value if column == "rk" else current.start_rk
if reach in (None, -1) or rk is None:
return False
rk = float(rk)
return any(
other is not current
and not other.is_deleted()
and other.reach == reach
and other.start_rk is not None
and other.end_rk is not None
and min(other.start_rk, other.end_rk) <= rk
<= max(other.start_rk, other.end_rk)
for other in self._data._data
)
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
@@ -62,23 +62,23 @@ class SetCommand(QUndoCommand):
class SetCommandSpec(QUndoCommand):
def __init__(self, data, row, column, new_value):
def __init__(self, parent, data, row, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._parent = parent
self._value = data[row]
self._column = column
if self._column == "name":
self._old = self._data[self._row].name
self._old = self._value.name
elif self._column == "reach":
self._old = self._data[self._row].reach
self._old_start_rk = self._data[self._row].start_rk
self._old_end_rk = self._data[self._row].end_rk
self._old = self._value.reach
self._old_start_rk = self._value.start_rk
self._old_end_rk = self._value.end_rk
elif self._column == "rk":
self._old = self._data[self._row].start_rk
self._old = self._value.start_rk
elif self._column == "temperature":
self._old = self._data[self._row].temperature
self._old = self._value.temperature
_type = float
if column == "name":
@@ -94,29 +94,31 @@ class SetCommandSpec(QUndoCommand):
def undo(self):
if self._column == "name":
self._data[self._row].name = self._old
self._value.name = self._old
elif self._column == "reach":
self._data[self._row].reach = self._old
self._data[self._row].start_rk = self._old_start_rk
self._data[self._row].end_rk = self._old_end_rk
self._value.reach = self._old
self._value.start_rk = self._old_start_rk
self._value.end_rk = self._old_end_rk
elif self._column == "rk":
self._data[self._row].start_rk = self._old
self._data[self._row].end_rk = self._old
self._value.start_rk = self._old
self._value.end_rk = self._old
elif self._column == "temperature":
self._data[self._row].temperature = self._old
self._value.temperature = self._old
self._parent.modified()
def redo(self):
if self._column == "name":
self._data[self._row].name = self._new
self._value.name = self._new
elif self._column == "reach":
self._data[self._row].reach = self._new
self._data[self._row].start_rk = self._new_rk
self._data[self._row].end_rk = self._new_rk
self._value.reach = self._new
self._value.start_rk = self._new_rk
self._value.end_rk = self._new_rk
elif self._column == "rk":
self._data[self._row].start_rk = self._new
self._data[self._row].end_rk = self._new
self._value.start_rk = self._new
self._value.end_rk = self._new
elif self._column == "temperature":
self._data[self._row].temperature = self._new
self._value.temperature = self._new
self._parent.modified()
class AddCommand(QUndoCommand):
@@ -32,6 +32,16 @@ class IcTemperatureTranslate(MainTranslate):
self._dict["rk"] = self._dict["unit_rk"]
self._dict["msg_rk_overlap_title"] = _translate(
"InitialConditionTemperature",
"Chainage already used"
)
self._dict["msg_rk_overlap_text"] = _translate(
"InitialConditionTemperature",
"An initial temperature condition is already defined at this "
"chainage on the selected reach."
)
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"temperature": self._dict["unit_temperature"],
+8 -2
View File
@@ -23,7 +23,7 @@ from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -76,7 +76,9 @@ class TableModel(PamhyrTableModel):
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -93,6 +95,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
value = self.date_to_elapsed_seconds(value)
self._undo.push(
SetDataCommand(
self._data, row, column, value
+2 -1
View File
@@ -109,7 +109,8 @@ class EditLateralContributionWindow(PamhyrWindow):
data=self._data,
undo=self._undo_stack,
trad=self._trad,
opt_data=self._study.time_system
opt_data=self._study.time_system,
start_date=self._study.date
)
table.setModel(self._table)
@@ -24,7 +24,7 @@ from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp,
timestamp_to_old_pamhyr_date_adists
timestamp_to_old_pamhyr_date_adists, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -90,7 +90,9 @@ class TableModel(PamhyrTableModel):
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date_adists(int(v))
else:
value = str(datetime.fromtimestamp(v))
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -107,6 +109,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
@@ -124,7 +124,8 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
data=self._data,
undo=self._undo_stack,
trad=self._trad,
opt_data=self._study.time_system
opt_data=self._study.time_system,
start_date=self._study.date
)
table.setModel(self._table)
+16 -2
View File
@@ -32,6 +32,7 @@ from PyQt5.QtGui import (
from PyQt5.QtWidgets import (
QApplication, QGraphicsScene, QGraphicsView,
QGraphicsItem, QGraphicsTextItem, QMenu,
QMessageBox,
)
from Model.Network.Node import Node
@@ -724,6 +725,16 @@ class GraphWidget(QGraphicsView):
def split_edge(self, item):
edge = item.edge
if len(edge.reach.profiles) < 3:
QMessageBox.information(
self,
self._trad["menu_split_edge"],
_translate(
"Network",
"No profile is available to split this reach."
)
)
return
dlg = SelectProfileDialog(
study=self.parent._study,
@@ -954,9 +965,10 @@ class GraphWidget(QGraphicsView):
self.set_current_edge(edge)
if not locked:
if items and type(items[0]) is NodeItem:
self._mouse_origin_x = pos.x()
self._mouse_origin_y = pos.y()
self._current_moved_node = items[0]
node_pos = self._current_moved_node.pos()
self._mouse_origin_x = node_pos.x()
self._mouse_origin_y = node_pos.y()
if not locked:
# Add nodes and edges
@@ -1011,6 +1023,8 @@ class GraphWidget(QGraphicsView):
self._mouse_origin_y)
)
)
# reset the current moved node
self._current_moved_node = None
self.update()
super(GraphWidget, self).mouseReleaseEvent(event)
+15 -9
View File
@@ -64,6 +64,7 @@ class SelectProfileDialog(PamhyrDialog):
self._reach = reach
self._profile = None
self._profiles = self._reach.reach.enabled_profiles
self.setup_combobox()
@@ -73,18 +74,23 @@ class SelectProfileDialog(PamhyrDialog):
list(
map(
lambda p: p.display_name(),
self._reach.reach.profiles)))
def accept(self):
profile = self.get_combobox_text("comboBox")
self._profile = next(
filter(
lambda p: p.display_name() == profile,
self._reach.reach.profiles
self._reach.reach.profiles[1:-1]
)
)
)
if len(self._profiles) == 0:
self.find(QDialogButtonBox, "buttonBox")\
.button(QDialogButtonBox.Ok)\
.setEnabled(False)
def accept(self):
index = self.find(QComboBox, "comboBox").currentIndex()
if not 0 <= index < len(self._profiles):
return
self._profile = self._profiles[index]
super(SelectProfileDialog, self).accept()
def reject(self):
+23 -10
View File
@@ -26,6 +26,7 @@ from PyQt5.QtCore import (
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
@@ -59,14 +60,23 @@ class TableModel(PamhyrTableModel):
return len(self._lst)
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
if not index.isValid():
return QVariant()
row = index.row()
column = index.column()
if role == Qt.BackgroundRole and not self._lst[row].enabled:
return QColor(190, 190, 190)
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
if self._headers[column] == "name":
return self._lst[row].name
name = self._lst[row].name
if not self._lst[row].enabled:
name += f" ({self._trad['disabled']})"
return name
return QVariant()
@@ -74,12 +84,19 @@ class TableModel(PamhyrTableModel):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
if self._headers[column] == "name":
suffix = f" ({self._trad['disabled']})"
if not self._lst[row].enabled and str(value).endswith(suffix):
value = str(value)[:-len(suffix)]
if str(self._lst[row].name) == str(value):
return False
elif self.is_same_data(index, value):
return False
try:
if self._headers[column] == "name":
self._undo.push(
@@ -110,14 +127,10 @@ class TableModel(PamhyrTableModel):
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
data_rows = {
id(pol): i for i, pol in enumerate(self._data._Pollutants._lst)
}
self._undo.push(
DelCommand(
self._data._Pollutants,
[data_rows[id(self._lst[row])] for row in rows
if 0 <= row < len(self._lst)],
[row for row in rows if 0 <= row < len(self._lst)],
self._data.ic_adists
)
)
+1
View File
@@ -32,6 +32,7 @@ class PollutantsTranslate(MainTranslate):
)
self._dict["x"] = _translate("Pollutants", "X (m)")
self._dict["disabled"] = _translate("Pollutants", "disabled")
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
+27 -21
View File
@@ -62,6 +62,8 @@ class PlotRKC(PamhyrPlot):
self._auto_relim_update = True
self._autoscale_update = False
self.profile = None
@property
def results(self):
return self.data
@@ -71,9 +73,19 @@ class PlotRKC(PamhyrPlot):
self.data = results
self._current_timestamp = max(self._timestamps)
@staticmethod
def reach_geometry(reach):
"""Return geometry values for profiles present in the results."""
return (
reach.geometry.get_rk_enabled_profiles(),
reach.geometry.get_z_min_enabled_profiles(),
reach.geometry.get_z_max_enabled_profiles()
)
@timer
def draw(self, highlight=None):
self.init_axes()
self.profile = None
if self.results is None:
return
@@ -104,7 +116,7 @@ class PlotRKC(PamhyrPlot):
def draw_bottom_with_bedload(self, reach):
results = self.results[self._current_res_id]
rk = reach.geometry.get_rk()
rk, _, _ = self.reach_geometry(reach)
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
@@ -136,8 +148,7 @@ class PlotRKC(PamhyrPlot):
for hs in lhs:
x = hs.input_section.rk
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
_, z_min, z_max = self.reach_geometry(reach)
self.canvas.axes.plot(
[x, x],
@@ -157,9 +168,7 @@ class PlotRKC(PamhyrPlot):
)
def draw_bottom_geometry(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
rk, z_min, _ = self.reach_geometry(reach)
self.line_rk_zmin = self.canvas.axes.plot(
rk, z_min,
@@ -170,10 +179,9 @@ class PlotRKC(PamhyrPlot):
self._river_bottom = z_min
def draw_water_elevation(self, reach):
if len(reach.geometry.profiles) != 0:
if len(reach.profiles) != 0:
result = self.results[self._current_res_id]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
rk, _, _ = self.reach_geometry(reach)
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
@@ -201,10 +209,9 @@ class PlotRKC(PamhyrPlot):
)
def draw_water_elevation_max(self, reach):
if len(reach.geometry.profiles) != 0:
if len(reach.profiles) != 0:
result = self.results[self._current_res_id]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
rk, _, _ = self.reach_geometry(reach)
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
@@ -250,9 +257,7 @@ class PlotRKC(PamhyrPlot):
self.canvas.draw_idle()
def draw_current(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
rk, z_min, z_max = self.reach_geometry(reach)
self.profile, = self.canvas.axes.plot(
[
@@ -349,8 +354,7 @@ class PlotRKC(PamhyrPlot):
def update_water_elevation(self):
result = self.results[self._current_res_id]
reach = result.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
rk, _, _ = self.reach_geometry(reach)
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
@@ -380,11 +384,13 @@ class PlotRKC(PamhyrPlot):
self.update_idle()
def update_current(self):
if not self._init or self.profile is None:
self.draw()
return
results = self.results[self._current_res_id]
reach = results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
rk, z_min, z_max = self.reach_geometry(reach)
cid = self._current_profile_id
self.profile.set_data(
@@ -396,7 +402,7 @@ class PlotRKC(PamhyrPlot):
def update_bottom_with_bedload(self):
results = self.results[self._current_res_id]
reach = results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
rk, _, _ = self.reach_geometry(reach)
# z = self.sl_compute_current_z(reach)
table = results.get("table")["zfd"]
+9 -9
View File
@@ -106,7 +106,7 @@ class PlotAdis(PamhyrPlot):
reach = self.results.river.reach(self._current_reach_id)
profile = reach.profile(self._current_profile_id)
if reach.geometry.number_profiles == 0:
if len(reach) == 0:
self._init = False
return
@@ -195,7 +195,7 @@ class PlotAdis_dx(PlotAdis):
def draw_data(self):
reach = self.results.river.reach(self._current_reach_id)
profile = reach.profile(self._current_profile_id)
x = reach.geometry.get_rk()
x = reach.get_rk()
self._lines = []
for i in range(len(self._current_pol_id)):
@@ -225,7 +225,7 @@ class PlotAdis_dx(PlotAdis):
reach = self.results.river.reach(self._current_reach_id)
profile = reach.profile(self._current_profile_id)
x = reach.geometry.get_rk()
x = reach.get_rk()
for i in range(len(self._current_pol_id)):
pol_id = self._current_pol_id[i]
@@ -244,7 +244,7 @@ class PlotAdis_dx(PlotAdis):
def draw_current(self):
reach = self.results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
rk = reach.get_rk()
self._current, = self.canvas.axes.plot(
[rk[self._current_profile_id], rk[self._current_profile_id]],
@@ -260,7 +260,7 @@ class PlotAdis_dx(PlotAdis):
self.canvas.axes.autoscale_view()
self.show_current()
reach = self.results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
rk = reach.get_rk()
self._current.set_data(
[rk[self._current_profile_id], rk[self._current_profile_id]],
self.canvas.axes.get_ylim()
@@ -269,13 +269,13 @@ class PlotAdis_dx(PlotAdis):
def get_min_and_max(self):
reach = self.results.river.reach(self._current_reach_id)
profile = reach.profile(self._current_profile_id)
x = reach.geometry.get_rk()
x = reach.get_rk()
y = [-inf]*reach.geometry.number_profiles
z = [inf]*reach.geometry.number_profiles
y = [-inf]*len(reach)
z = [inf]*len(reach)
for pol_id in self._current_pol_id:
if self.val_id[self._key] > 0 and self._type_pol[pol_id] == 1:
for i in range(reach.geometry.number_profiles):
for i in range(len(reach)):
y[i] = max(0.0, y[i])
z[i] = min(0.0, z[i])
else:
+215
View File
@@ -0,0 +1,215 @@
# PlotTemperature.py -- Pamhyr
# Copyright (C) 2026 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.
# -*- coding: utf-8 -*-
import numpy as np
from matplotlib.cm import ScalarMappable
from matplotlib.collections import PolyCollection
from matplotlib.colors import Normalize
from View.Results.PlotXY import PlotXY
class PlotTemperature(PlotXY):
def __init__(self, canvas=None, trad=None, toolbar=None,
results=None, reach_id=0, profile_id=0,
pol_id=1, parent=None):
super(PlotTemperature, self).__init__(
canvas=canvas,
trad=trad,
toolbar=toolbar,
results=results,
reach_id=reach_id,
profile_id=profile_id,
res_id=[0],
parent=parent,
)
self._current_pol_id = pol_id
self._global_ranges = {}
self._temperature_zones = None
self._colorbar = None
self._auto_relim_update = False
self._autoscale_update = False
@property
def results(self):
return self.data
@results.setter
def results(self, results):
self.data = results
self._timestamps = sorted(results.get("timestamps"))
self._current_timestamp = self._timestamps[-1]
self._global_ranges.clear()
def draw(self, highlight=None):
if self._colorbar is not None:
self._colorbar.remove()
self._colorbar = None
self.init_axes()
reach = self.results.river.reach(self._current_reach_id)
if reach.geometry.number_profiles == 0:
self._init = False
return
temperatures = self._temperatures(reach)
norm = self._temperature_norm()
self.draw_profiles(reach, self.results.river.reachs)
self.draw_guide_lines(reach)
self._draw_temperature_zones(reach, temperatures, norm)
self.draw_current(reach)
mappable = self._temperature_zones
if mappable is None:
mappable = ScalarMappable(norm=norm, cmap="coolwarm")
self._colorbar = self.canvas.figure.colorbar(
mappable, ax=self.canvas.axes
)
self._colorbar.set_label(self._trad["unit_temperature"])
# self.canvas.axes.set_title(
# f"{self._trad['temperature_map']} — {reach.name}"
# )
self.canvas.axes.set_aspect("auto")
self._zoom_to_reach_bbox(reach)
self.canvas.draw_idle()
self.toolbar_update()
self._init = True
def _draw_temperature_zones(self, reach, temperatures, norm):
profiles = reach.profiles
polygons = []
for index in range(len(profiles) - 1):
current = profiles[index].geometry
following = profiles[index + 1].geometry
if current.number_points == 0 or following.number_points == 0:
continue
polygons.append([
(current.x()[0], current.y()[0]),
(current.x()[-1], current.y()[-1]),
(following.x()[-1], following.y()[-1]),
(following.x()[0], following.y()[0]),
])
if not polygons:
self._temperature_zones = None
return
self._temperature_zones = PolyCollection(
polygons,
cmap="coolwarm",
norm=norm,
edgecolors="none",
alpha=0.8,
zorder=1,
)
self._temperature_zones.set_array(
self._segment_temperatures(temperatures)
)
self.canvas.axes.add_collection(self._temperature_zones)
def update(self):
if not self._init or self._temperature_zones is None:
self.draw()
return
reach = self.results.river.reach(self._current_reach_id)
self._temperature_zones.set_array(
self._segment_temperatures(self._temperatures(reach))
)
self.canvas.draw_idle()
def set_reach(self, reach_id):
self._current_reach_id = reach_id
self._current_profile_id = 0
self.draw()
def set_profile(self, profile_id):
self._current_profile_id = profile_id
reach = self.results.river.reach(self._current_reach_id)
profile = reach.profile(profile_id)
self.plot_selected.set_data(
profile.geometry.x(), profile.geometry.y()
)
self.canvas.draw_idle()
def set_pollutant(self, pol_id):
self._current_pol_id = pol_id
self.draw()
def set_timestamp(self, timestamp):
self._current_timestamp = timestamp
self.update()
def _temperatures(self, reach):
return np.asarray([
profile.get_ts_key(self._current_timestamp, "pols")[
self._current_pol_id
][0]
for profile in reach.profiles
], dtype=float)
@staticmethod
def _segment_temperatures(temperatures):
return (temperatures[:-1] + temperatures[1:]) / 2.0
def _temperature_norm(self):
minimum, maximum = self._global_temperature_range()
if minimum == maximum:
maximum = minimum + 1.0
return Normalize(vmin=minimum, vmax=maximum)
def _zoom_to_reach_bbox(self, reach):
x_values = [
np.asarray(profile.geometry.x(), dtype=float)
for profile in reach.profiles
if len(profile.geometry.x()) != 0
]
y_values = [
np.asarray(profile.geometry.y(), dtype=float)
for profile in reach.profiles
if len(profile.geometry.y()) != 0
]
if not x_values or not y_values:
return
x = np.concatenate(x_values)
y = np.concatenate(y_values)
x_min, x_max = float(np.min(x)), float(np.max(x))
y_min, y_max = float(np.min(y)), float(np.max(y))
x_margin = max((x_max - x_min) * 0.05, 1.0)
y_margin = max((y_max - y_min) * 0.05, 1.0)
self.canvas.axes.set_xlim(x_min - x_margin, x_max + x_margin)
self.canvas.axes.set_ylim(y_min - y_margin, y_max + y_margin)
def _global_temperature_range(self):
pol_id = self._current_pol_id
if pol_id in self._global_ranges:
return self._global_ranges[pol_id]
temperatures = []
for reach in self.results.river.reachs:
for profile in reach.profiles:
for timestamp in self._timestamps:
values = profile.get_ts_key(timestamp, "pols")
if values is not None:
temperatures.append(values[pol_id][0])
temperatures = np.asarray(temperatures, dtype=float)
temperatures = temperatures[np.isfinite(temperatures)]
value_range = (
(float(np.min(temperatures)), float(np.max(temperatures)))
if temperatures.size
else (0.0, 1.0)
)
self._global_ranges[pol_id] = value_range
return value_range
+2 -3
View File
@@ -1152,7 +1152,6 @@ class ResultsWindow(PamhyrWindow):
z = table["Z"][:, profile.global_index]
q = table["Q"][:, profile.global_index]
v = table["V"][:, profile.global_index]
zfd = table["zfd"][:, profile.global_index]
if self._current_results == 2:
reach1 = self._results[0].river.reach(self._reach)
@@ -1171,8 +1170,8 @@ class ResultsWindow(PamhyrWindow):
v2 = table["V"][:, profile2.global_index]
if "bed_elevation" in y:
if reach.has_bedload():
z_min = zfd
if reach.has_bedload() and "zfd" in table:
z_min = table["zfd"][:, profile.global_index]
else:
z_min = [profile.geometry.z_min()] * len(self._timestamps)
+1 -1
View File
@@ -726,7 +726,7 @@ class ResultsWindowAdisTS(PamhyrWindow):
dict_x = self._trad.get_dict("values_x")
dict_y = self._trad.get_dict("values_y_pol")
my_dict = {}
my_dict[dict_x["rk"]] = reach.geometry.get_rk()
my_dict[dict_x["rk"]] = reach.get_rk()
val_id = {}
val_id["unit_C"] = 0
val_id["unit_M"] = 2
+34
View File
@@ -47,6 +47,7 @@ from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.Results.PlotSedAdis import PlotAdis_dx, PlotAdis_dt
from View.Results.PlotTemperature import PlotTemperature
from View.Results.CustomPlot.Plot import CustomPlot
from View.Results.CustomExport.CustomExportAdis import (
@@ -225,6 +226,33 @@ class ResultsWindowAdisTT(PamhyrWindow):
)
self.plot_cdx.draw()
self.canvas_temperature_map = MplCanvas(width=5, height=4, dpi=100)
self.canvas_temperature_map.setObjectName("canvas_temperature_map")
self.toolbar_temperature_map = PamhyrPlotToolbar(
self.canvas_temperature_map, self, items=[
"home", "move", "zoom", "save", "iso", "back/forward"
]
)
temperature_map_tab = QWidget()
temperature_map_layout = QVBoxLayout(temperature_map_tab)
temperature_map_layout.addWidget(self.toolbar_temperature_map)
temperature_map_layout.addWidget(self.canvas_temperature_map)
self.find(QTabWidget, "tabWidget_c").addTab(
temperature_map_tab,
self._trad["temperature_map"]
)
self.plot_temperature = PlotTemperature(
canvas=self.canvas_temperature_map,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=self._current_pol_id[0],
trad=self._trad,
toolbar=self.toolbar_temperature_map,
parent=self,
)
self.plot_temperature.draw()
# The AdisTT window only displays temperature plots. The code below
# belongs to the sediment/pollutant result window and its layouts are
# intentionally absent from ResultsAdisTT.ui.
@@ -507,6 +535,7 @@ class ResultsWindowAdisTT(PamhyrWindow):
self._reach_id = reach_id
self.plot_cdt.set_reach(reach_id)
self.plot_cdx.set_reach(reach_id)
self.plot_temperature.set_reach(reach_id)
self.update_table_selection_reach(reach_id)
self.update_table_selection_profile(0)
@@ -515,6 +544,7 @@ class ResultsWindowAdisTT(PamhyrWindow):
self._profile_id = profile_id
self.plot_cdt.set_profile(profile_id)
self.plot_cdx.set_profile(profile_id)
self.plot_temperature.set_profile(profile_id)
self.update_table_selection_profile(profile_id)
@@ -522,10 +552,12 @@ class ResultsWindowAdisTT(PamhyrWindow):
self._current_pol_id = [p+1 for p in pol_id] # rm total_sediment
self.plot_cdt.set_pollutant(self._current_pol_id)
self.plot_cdx.set_pollutant(self._current_pol_id)
self.plot_temperature.set_pollutant(self._current_pol_id[0])
if timestamp is not None:
self.plot_cdt.set_timestamp_preserve_view(timestamp)
self.plot_cdx.set_timestamp_preserve_view(timestamp)
self.plot_temperature.set_timestamp(timestamp)
self._table["raw_data"].set_timestamp(timestamp)
@@ -574,6 +606,8 @@ class ResultsWindowAdisTT(PamhyrWindow):
self.plot_cdt.draw()
self.plot_cdx.draw()
self.plot_temperature.results = self._results
self.plot_temperature.draw()
def _reload_slider(self):
self._slider_time = self.find(QSlider, f"horizontalSlider_time")
+6
View File
@@ -46,6 +46,12 @@ class ResultsTranslate(MainTranslate):
self._dict['solver'] = _translate("Results", "Solver")
self._dict['x'] = _translate("Results", "X (m)")
self._dict["temperature_map"] = _translate(
"Results", "Reach temperature map"
)
self._dict["temperature"] = _translate(
"Results", "Temperature"
)
self._dict['label_bottom'] = _translate("Results", "Bottom")
self._dict['label_water'] = _translate("Results", "Water elevation")
-7
View File
@@ -78,11 +78,6 @@ class SolverParametersWindow(PamhyrWindow):
for st in solver_type_list:
self._undo_stack[st] = QUndoStack()
self._undo_sc = QShortcut(QKeySequence.Undo, self)
self._redo_sc = QShortcut(QKeySequence.Redo, self)
self._undo_sc.activated.connect(self._undo)
self._redo_sc.activated.connect(self._redo)
def setup_table(self):
self._table = {}
self._tab_widget = self.find(QTabWidget, f"tabWidget")
@@ -134,12 +129,10 @@ class SolverParametersWindow(PamhyrWindow):
def _undo(self):
tab = self.current_tab()
self._table[tab].undo()
self._set_current_reach()
def _redo(self):
tab = self.current_tab()
self._table[tab].redo()
self._set_current_reach()
def _copy(self):
logger.info("TODO: copy")
+28 -2
View File
@@ -19,7 +19,7 @@
import logging
import traceback
from tools import trace, timer
from tools import trace, timer, parse_datetime
from Model.Except import NotImplementedMethodeError
@@ -34,8 +34,11 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QStyledItemDelegate, QHeaderView,
QMessageBox,
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
@@ -59,6 +62,27 @@ class PamhyrTextDelegate(QStyledItemDelegate):
class PamhyrTableModel(QAbstractTableModel):
def date_to_elapsed_seconds(self, value):
"""Convert an entered date to seconds since the study start date."""
try:
entered_date = parse_datetime(value)
except ValueError as error:
QMessageBox.warning(
self._table_view,
_translate("PamhyrTableModel", "Invalid date/time"),
_translate(
"PamhyrTableModel",
"The entered date/time is invalid.\n"
"Expected formats:\n"
"YYYY-MM-DD [HH:MM[:SS]],\n"
"DD-MM-YYYY [HH:MM[:SS]] or\n"
"DD/MM/YYYY [HH:MM[:SS]]."
)
)
raise error
return (entered_date - self._start_date).total_seconds()
def _setup_delegates(self):
if self._table_view is None:
return
@@ -85,7 +109,8 @@ class PamhyrTableModel(QAbstractTableModel):
undo=None,
opt_data=None,
options=["rows_selection"],
parent=None):
parent=None,
start_date=None):
super(PamhyrTableModel, self).__init__()
self._table_view = table_view
@@ -102,6 +127,7 @@ class PamhyrTableModel(QAbstractTableModel):
self._options = options
self._undo = undo
self._lst = []
self._start_date = start_date
self._setup_delegates()
self._setup_lst()
+14 -19
View File
@@ -23,7 +23,7 @@ from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -77,7 +77,9 @@ class TableModel(PamhyrTableModel):
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -94,6 +96,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
value = self.date_to_elapsed_seconds(value)
self._undo.push(
SetDataCommand(
self._data, row, column, value
@@ -145,35 +151,24 @@ class TableModel(PamhyrTableModel):
if row <= 0:
return
target = row + 2
self.beginMoveRows(parent, row - 1, row - 1, parent, target)
self._undo_stack.push(
self.layoutAboutToBeChanged.emit()
self._undo.push(
MoveCommand(
self._data, "up", row
)
)
self.endMoveRows()
self.update()
def move_down(self, index, parent=QModelIndex()):
row = index.row()
if row >= len(self._data):
def move_down(self, row, parent=QModelIndex()):
if row < 0 or row >= len(self._data) - 1:
return
target = row
self.beginMoveRows(parent, row + 1, row + 1, parent, target)
self._undo_stack.push(
self.layoutAboutToBeChanged.emit()
self._undo.push(
MoveCommand(
self._data, "down", row
)
)
self.endMoveRows()
self.update()
def paste(self, row, header, data):
+37 -43
View File
@@ -31,17 +31,17 @@ class SetDataCommand(QUndoCommand):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._value = self._data.get_i(index)
self._column = column
self._old = self._data.get_i(self._index)[self._column]
self._old = self._value[self._column]
_type = self._data.get_type_column(self._column)
self._new = _type(new_value)
def undo(self):
self._data._set_i_c_v(self._index, self._column, self._old)
self._data.set_data_value(self._value, self._column, self._old)
def redo(self):
self._data._set_i_c_v(self._index, self._column, self._new)
self._data.set_data_value(self._value, self._column, self._new)
class AddCommand(QUndoCommand):
@@ -53,13 +53,13 @@ class AddCommand(QUndoCommand):
self._new = None
def undo(self):
self._data.delete_i([self._index])
self._data.set_deleted([self._new], True)
def redo(self):
if self._new is None:
self._new = self._data.add(self._index)
else:
self._data.insert(self._index, self._new)
self._data.set_deleted([self._new], False)
class DelCommand(QUndoCommand):
@@ -75,10 +75,10 @@ class DelCommand(QUndoCommand):
self._wp.sort()
def undo(self):
self._data.set_as_not_deleted_i(self._rows)
self._data.set_deleted([wp for row, wp in self._wp], False)
def redo(self):
self._data.set_as_deleted_i(self._rows)
self._data.set_deleted([wp for row, wp in self._wp], True)
class SortCommand(QUndoCommand):
@@ -89,27 +89,17 @@ class SortCommand(QUndoCommand):
self._reverse = _reverse
self._old = self._data.data
self._indexes = None
self._new = sorted(
self._old,
key=lambda value: value[0],
reverse=self._reverse
)
def undo(self):
ll = self._data.data
self._data.sort(
key=lambda x: self._indexes[ll.index(x)]
)
self._data.reorder(self._old)
def redo(self):
self._data.sort(
_reverse=self._reverse,
key=lambda x: x[0]
)
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None
self._data.reorder(self._new)
class MoveCommand(QUndoCommand):
@@ -117,20 +107,16 @@ class MoveCommand(QUndoCommand):
QUndoCommand.__init__(self)
self._data = data
self._up = up == "up"
self._i = i
self._old = self._data.data
self._new = self._old.copy()
other = i - 1 if up == "up" else i + 1
self._new[i], self._new[other] = self._new[other], self._new[i]
def undo(self):
if self._up:
self._data.move_up(self._i)
else:
self._data.move_down(self._i)
self._data.reorder(self._old)
def redo(self):
if self._up:
self._data.move_up(self._i)
else:
self._data.move_down(self._i)
self._data.reorder(self._new)
class PasteCommand(QUndoCommand):
@@ -141,15 +127,18 @@ class PasteCommand(QUndoCommand):
self._row = row
self._wps = list(wps)
self._wps.reverse()
self._inserted = False
def undo(self):
self._data.delete_i(
range(self._row, self._row + len(self._wps))
)
self._data.set_deleted(self._wps, True)
def redo(self):
for wp in self._wps:
self._data.insert(self._row, wp)
if not self._inserted:
for wp in self._wps:
self._data.insert(self._row, wp)
self._inserted = True
else:
self._data.set_deleted(self._wps, False)
class DuplicateCommand(QUndoCommand):
@@ -160,10 +149,15 @@ class DuplicateCommand(QUndoCommand):
self._rows = rows
self._wp = deepcopy(bc)
self._wp.reverse()
self._inserted = False
def undo(self):
self._data.delete(self._wp)
self._data.set_deleted(self._wp, True)
def redo(self):
for bc in self._wp:
self._data.insert(self._rows[0], bc)
if not self._inserted:
for bc in self._wp:
self._data.insert(self._rows[0], bc)
self._inserted = True
else:
self._data.set_deleted(self._wp, False)
+2 -1
View File
@@ -121,7 +121,8 @@ class EditWeatherParametersWindow(PamhyrWindow):
data=self._data,
undo=self._undo_stack,
trad=self._trad,
opt_data=self._study.time_system
opt_data=self._study.time_system,
start_date=self._study.date
)
table.setModel(self._table)
+62 -2
View File
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
QComboBox, QMessageBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -245,6 +245,10 @@ class WeatherParametersTableModel(PamhyrTableModel):
p for p in _edge.reach.profiles
if p.pamhyr_id == value
)
if self._overlaps_existing_interval(
row, begin_section=_begin_rk):
self._show_overlap_warning()
return False
self._undo.push(
SetBeginCommand(
self._data, global_row, _begin_rk
@@ -256,18 +260,32 @@ class WeatherParametersTableModel(PamhyrTableModel):
p for p in _edge.reach.profiles
if p.pamhyr_id == value
)
if self._overlaps_existing_interval(
row, end_section=_end_rk):
self._show_overlap_warning()
return False
self._undo.push(
SetEndCommand(
self._data, global_row, _end_rk
)
)
elif self._headers[column] == "reach":
new_reach = self._river.edge(value)
overlaps = self._overlaps_existing_interval(
row, reach=new_reach
)
current = self._lst[row]
clear_interval = overlaps and current.reach is None
if overlaps and not clear_interval:
self._show_overlap_warning()
return False
edge = self._river.edge(value)
if not edge.reach.profiles:
return False
self._undo.push(
SetEdgeCommand(
self._data, global_row, edge
self._data, global_row, new_reach,
clear_interval=clear_interval
)
)
except Exception as e:
@@ -283,6 +301,48 @@ class WeatherParametersTableModel(PamhyrTableModel):
self.dataChanged.emit(index, index)
return True
def _overlaps_existing_interval(self, row, reach=None,
begin_section=None, end_section=None):
current = self._lst[row]
reach = current.reach if reach is None else reach
if reach is None:
return False
if begin_section is None:
begin_section = (
reach.reach.profiles[0]
if reach is not current.reach and reach.reach.profiles
else current.begin_section
)
if end_section is None:
end_section = (
reach.reach.profiles[-1]
if reach is not current.reach and reach.reach.profiles
else current.end_section
)
if begin_section is None or end_section is None:
return False
lower, upper = sorted((begin_section.rk, end_section.rk))
return any(
other is not current
and not other.is_deleted()
and other.type == current.type
and other.reach is reach
and other.begin_section is not None
and other.end_section is not None
and max(lower, min(other.begin_rk, other.end_rk))
< min(upper, max(other.begin_rk, other.end_rk))
for other in self._data.lst
)
def _show_overlap_warning(self):
QMessageBox.warning(
self._table_view,
self._trad["msg_rk_overlap_title"],
self._trad["msg_rk_overlap_text"]
)
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row)
+1 -1
View File
@@ -120,7 +120,7 @@ class SetEndCommand(QUndoCommand):
class SetEdgeCommand(QUndoCommand):
def __init__(self, wps, index, edge):
def __init__(self, wps, index, edge, clear_interval=False):
QUndoCommand.__init__(self)
self._wps = wps
+8
View File
@@ -41,6 +41,14 @@ class WeatherParametersTranslate(MainTranslate):
"These values are applied to sections that have not been "
"defined using time series."
)
self._dict["msg_rk_overlap_title"] = _translate(
"WeatherParameters", "Overlapping interval"
)
self._dict["msg_rk_overlap_text"] = _translate(
"WeatherParameters",
"Two weather-parameter intervals of the same type cannot "
"overlap on the same reach."
)
self._dict["rk"] = self._dict["unit_rk"]
+537 -283
View File
File diff suppressed because it is too large. Load diff
+53 -1
View File
@@ -22,7 +22,7 @@ import logging
import sqlite3
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from colorama import Fore
@@ -239,6 +239,58 @@ def timestamp(dt: datetime):
return dt.timestamp()
SUPPORTED_DATETIME_FORMATS = (
"%d-%m-%Y %H:%M:%S.%f",
"%d-%m-%Y %H:%M:%S",
"%d-%m-%Y %H:%M",
"%d-%m-%Y",
"%d/%m/%Y %H:%M:%S.%f",
"%d/%m/%Y %H:%M:%S",
"%d/%m/%Y %H:%M",
"%d/%m/%Y",
"%d-%m-%y %H:%M:%S",
"%d/%m/%y %H:%M:%S",
)
def parse_datetime(value):
"""Parse a supported date representation into a naive UTC datetime."""
if isinstance(value, datetime):
parsed = value
else:
text = str(value).strip()
if text.endswith(("Z", "z")):
text = text[:-1] + "+00:00"
try:
parsed = datetime.fromisoformat(text)
except ValueError as iso_error:
for date_format in SUPPORTED_DATETIME_FORMATS:
try:
parsed = datetime.strptime(text, date_format)
break
except ValueError:
continue
else:
raise ValueError(
f"Unsupported date format: {value!r}. Expected an ISO "
"date/time or one of: "
+ ", ".join(SUPPORTED_DATETIME_FORMATS)
) from iso_error
# If the parsed datetime has timezone information,
# convert it to UTC and remove the timezone info
if parsed.tzinfo is not None:
parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
return parsed
def format_datetime(value):
"""Return a datetime using the canonical application format."""
return parse_datetime(value).strftime("%Y-%m-%d %H:%M:%S")
def date_iso_to_timestamp(date: str):
if type(date) is str:
return timestamp(datetime.fromisoformat(date))