// Copyright (C) Stichting Deltares 2026. All rights reserved. // // This file is part of the application DAM - UI. // // DAM - UI is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Xml.Serialization; using Deltares.Dam.Data.Sensors.Specifications; using Deltares.Geometry; using Deltares.Geotechnics.SurfaceLines; using Deltares.Standard; using Deltares.Standard.Attributes; using Deltares.Standard.IO.Xml; using Deltares.Standard.Reflection; using Deltares.Standard.Specifications; using Deltares.Standard.Specifications.Extensions; using XmlSerializer = Deltares.Standard.IO.Xml.XmlSerializer; namespace Deltares.Dam.Data.Sensors; /// /// Association class for sensor data and a location /// [Serializable] public class SensorLocation : IDomain { /// /// Constructor is needed for XML deserialization /// [Browsable(false)] public SensorLocation() { SourceTypePl1WaterLevelAtRiver = DataSourceTypeSensors.LocationData; SourceTypePl1WaterLevelAtPolder = DataSourceTypeSensors.LocationData; } /// /// Gets or sets the DAM location. /// /// /// The location. /// [XmlIgnore] [Specification(typeof(NotNullSpecification))] [Browsable(false)] public Location Location { get; set; } /// /// Gets the location Name. /// /// /// If the Name is empty it could indicate that there is no reference /// to a valid location /// [XmlIgnore] [PropertyOrder(1, 1)] public string LocationName { get { return Location != null ? Location.Name : string.Empty; } } /// /// Gets or sets the sensor group. /// /// /// The group. /// [Specification(typeof(NotNullSpecification))] [PropertyOrder(1, 5)] public Group Group { get; set; } /// /// Gets the group ID. /// /// /// If the ID has value -1 then the there is no valid reference (null) /// or the group is transient. /// [XmlIgnore] [Browsable(false)] public int GroupID { get { if (Group == null) { return -1; } return Group.ID; } } /// /// Gets the sensor selection. /// [XmlIgnore] [Browsable(false)] public IEnumerable Sensors { get { if (Group == null) { return new Sensor[0]; } return Group.Selection; } } /// /// Gets or sets the source type PL3. /// /// /// The source type PL3. /// [Specification(typeof(SensorOrLocationData))] [PropertyOrder(1, 12)] [XmlOldName("PL3")] public DataSourceTypeSensors SourceTypePl3 { get; set; } /// /// Gets or sets the source type PL4. /// /// /// The source type PL4. /// [Specification(typeof(SensorOrLocationData))] [PropertyOrder(1, 13)] [XmlOldName("PL4")] public DataSourceTypeSensors SourceTypePl4 { get; set; } /// /// Gets or sets the source type PL1 water level at river. /// /// /// The source type PL1 water level at river. /// [Specification(typeof(SensorOrLocationData))] [PropertyOrder(1, 6)] [XmlOldName("PL1WaterLevelAtRiver")] public DataSourceTypeSensors SourceTypePl1WaterLevelAtRiver { get; set; } /// /// Gets or sets the source type PL1 pl line offset below dike top at river. /// /// /// The source type PL1 pl line offset below dike top at river. /// [Specification(typeof(IgnoreOrLocationData))] [PropertyOrder(1, 7)] [XmlOldName("PL1PLLineOffsetBelowDikeTopAtRiver")] public DataSourceTypeSensors SourceTypePl1PlLineOffsetBelowDikeTopAtRiver { get; set; } /// /// Gets or sets the source type PL1 pl line offset below dike top at polder. /// /// /// The source type PL1 pl line offset below dike top at polder. /// [Specification(typeof(IgnoreOrLocationData))] [PropertyOrder(1, 8)] [XmlOldName("PL1PLLineOffsetBelowDikeTopAtPolder")] public DataSourceTypeSensors SourceTypePl1PlLineOffsetBelowDikeTopAtPolder { get; set; } [Specification(typeof(IgnoreOrLocationData))] [PropertyOrder(1, 9)] public DataSourceTypeSensors SourceTypePl1PlLineOffsetBelowShoulderBaseInside { get; set; } /// /// Gets or sets the source type PL1 pl line offset below dike toe at polder. /// /// /// The source type PL1 pl line offset below dike toe at polder. /// [Specification(typeof(IgnoreOrLocationData))] [PropertyOrder(1, 10)] [XmlOldName("PL1PLLineOffsetBelowDikeToeAtPolder")] public DataSourceTypeSensors SourceTypePl1PlLineOffsetBelowDikeToeAtPolder { get; set; } /// /// Gets or sets the source type PL1 water level at polder. /// /// /// The source type PL1 water level at polder. /// [Specification(typeof(SensorOrLocationData))] [PropertyOrder(1, 11)] [XmlOldName("PL1WaterLevelAtPolder")] public DataSourceTypeSensors SourceTypePl1WaterLevelAtPolder { get; set; } /// /// Gets the sensor count. /// [Browsable(false)] public int SensorCount { get { return Group?.SensorCount ?? 0; } } [Browsable(false)] public SurfaceLine2 SurfaceLine { get { return Location.SurfaceLine2; } } [PropertyOrder(1, 3)] public string Alias { get; set; } [Browsable(false)] public string Profile { get; set; } /// /// Gets or sets the GetGroups function (injectable list, for UI purposes). /// /// /// The list of available Groups. /// [XmlIgnore] [Browsable(false)] public static Func> GetGroups { get; set; } /// /// Resets the group identifier. /// /// The identifier. public void ResetGroupID(int id) { Group.ID = id; } /// /// Gets the requested sensor value. /// /// The expression. /// The sensor values. /// The sensor. /// /// /// sensorValues /// or /// sensor /// /// The given sensor is not an item/key in the table of sensor values public double? GetValue(Expression> expression, IDictionary sensorValues, Sensor sensor) { if (sensorValues == null) { throw new ArgumentNullException("sensorValues"); } if (sensor == null) { throw new ArgumentNullException("sensor"); } if (!sensorValues.ContainsKey(sensor)) { throw new ArgumentException("The given sensor is not an item/key in the table of sensor values"); } string memberName = StaticReflection.GetMemberName(expression); if (memberName == MemberNames.WaterLevelAtRiver) { if (SourceTypePl1WaterLevelAtRiver == DataSourceTypeSensors.Sensor) { return sensorValues[sensor]; } if (SourceTypePl1WaterLevelAtRiver == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].RiverLevel; } } if (memberName == MemberNames.PolderLevel) { if (SourceTypePl1WaterLevelAtPolder == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].PolderLevel; } if (SourceTypePl1WaterLevelAtPolder == DataSourceTypeSensors.Sensor) { return sensorValues[sensor]; } } if (memberName == MemberNames.PL3) { if (SourceTypePl3 == DataSourceTypeSensors.Sensor) { return sensorValues[sensor]; } if (SourceTypePl3 == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].HeadPl3; } } if (memberName == MemberNames.PL4) { if (SourceTypePl4 == DataSourceTypeSensors.Sensor) { return sensorValues[sensor]; } if (SourceTypePl4 == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].HeadPl4; } } if (memberName == MemberNames.Pl1PlLineOffsetBelowDikeToeAtPolder) { if (SourceTypePl1PlLineOffsetBelowDikeToeAtPolder == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].PlLineOffsetBelowDikeToeAtPolder; } } if (memberName == MemberNames.Pl1PlLineOffsetBelowDikeTopAtPolder) { if (SourceTypePl1PlLineOffsetBelowDikeTopAtPolder == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].PlLineOffsetBelowDikeTopAtPolder; } } if (memberName == MemberNames.Pl1PlLineOffsetBelowDikeTopAtRiver) { if (SourceTypePl1PlLineOffsetBelowDikeTopAtRiver == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].PlLineOffsetBelowDikeTopAtRiver; } } if (memberName == MemberNames.Pl1PlLineOffsetBelowShoulderBaseInside) { if (SourceTypePl1PlLineOffsetBelowShoulderBaseInside == DataSourceTypeSensors.LocationData) { return Location.Scenarios[0].PlLineOffsetBelowShoulderBaseInside; } } return null; } /// /// Determines whether this instance is valid. /// /// /// true if this instance is valid; otherwise, false. /// public bool IsValid() { if (IsLocationDataAsDataSourceUsed() && Location != null) { if (Location.Scenarios.Count != 1) { return false; } } if (Validator.Validate(this).Any()) { return false; } return true; } /// /// Serializes this instance. /// /// public string Serialize() { var xmlSerializer = new XmlSerializer(); return xmlSerializer.SerializeToString(this); } /// /// Deserializes the specified XML. /// /// The XML. /// public static SensorLocation Deserialize(string xml) { var xmlDeserializer = new XmlDeserializer(); return (SensorLocation) xmlDeserializer.XmlDeserializeFromString(xml, typeof(SensorLocation)); } /// /// Gets the domain for properties in the UI. /// /// The property. /// public ICollection GetDomain(string property) { switch (property) { case "Group": return GetGroupsDomain(); default: return null; } } /// /// Gets the PiezometricHead type sensors sorted by relative location along profile. /// /// Type of the pl line. /// internal SortedDictionary GetSensorsSortedByRelativeLocationAlongProfile(PLLineType plLineType) { IDictionary calculatedRelativeLocations = BuildRelativeLocationTable(); var table = new SortedDictionary(); // leave out the water level and polder level sensors IEnumerable candidateSensors = Sensors.GetBySpecification(new PiezometricHeadSensorSpecification()); foreach (Sensor sensor in candidateSensors) { if (sensor.PLLineMappings.Contains(plLineType)) { double relativeLocation = sensor.RelativeLocationSpecified ? sensor.RelativeLocation : calculatedRelativeLocations[sensor]; if (table.ContainsKey(relativeLocation)) { throw new InvalidOperationException( "Error creating lookup table with sensors sorted by relative location along profile. The x-location " + relativeLocation + " already exists. Sensor " + sensor); } table.Add(relativeLocation, sensor); } } return table; } /// /// Builds the relative location table. /// /// internal IDictionary BuildRelativeLocationTable() { GeometryPoint surfaceLevelOutside = SurfaceLine.CharacteristicPoints.GetGeometryPoint(CharacteristicPointType.SurfaceLevelOutside); if (surfaceLevelOutside == null) { throw new InvalidOperationException("No SurfaceLevelOutside point on surface line defined"); } double startPoint = surfaceLevelOutside.X; var dict = new Dictionary(); foreach (Sensor sensor in Sensors) { double relativeLocation = startPoint + (Location.XRdDikeLine - sensor.XRd); dict.Add(sensor, relativeLocation); } return dict; } /// /// Checks if LocationData is used as a source. /// /// private bool IsLocationDataAsDataSourceUsed() { if (SourceTypePl1WaterLevelAtRiver == DataSourceTypeSensors.LocationData || SourceTypePl1WaterLevelAtPolder == DataSourceTypeSensors.LocationData || SourceTypePl3 == DataSourceTypeSensors.LocationData || SourceTypePl4 == DataSourceTypeSensors.LocationData || SourceTypePl1PlLineOffsetBelowDikeToeAtPolder == DataSourceTypeSensors.LocationData || SourceTypePl1PlLineOffsetBelowDikeTopAtPolder == DataSourceTypeSensors.LocationData || SourceTypePl1PlLineOffsetBelowDikeTopAtRiver == DataSourceTypeSensors.LocationData || SourceTypePl1PlLineOffsetBelowShoulderBaseInside == DataSourceTypeSensors.LocationData) { return true; } return false; } private ICollection GetGroupsDomain() { if (GetGroups == null) { return null; } return GetGroups(this).ToList(); } internal static class MemberNames { internal static readonly string WaterLevelAtRiver = StaticReflection.GetMemberName(x => x.SourceTypePl1WaterLevelAtRiver); internal static readonly string PolderLevel = StaticReflection.GetMemberName(x => x.SourceTypePl1WaterLevelAtPolder); internal static readonly string PL3 = StaticReflection.GetMemberName(x => x.SourceTypePl3); internal static readonly string PL4 = StaticReflection.GetMemberName(x => x.SourceTypePl4); internal static readonly string Pl1PlLineOffsetBelowDikeToeAtPolder = StaticReflection.GetMemberName(x => x.SourceTypePl1PlLineOffsetBelowDikeToeAtPolder); internal static readonly string Pl1PlLineOffsetBelowDikeTopAtPolder = StaticReflection.GetMemberName(x => x.SourceTypePl1PlLineOffsetBelowDikeTopAtPolder); internal static readonly string Pl1PlLineOffsetBelowDikeTopAtRiver = StaticReflection.GetMemberName(x => x.SourceTypePl1PlLineOffsetBelowDikeTopAtRiver); internal static readonly string Pl1PlLineOffsetBelowShoulderBaseInside = StaticReflection.GetMemberName(x => x.SourceTypePl1PlLineOffsetBelowShoulderBaseInside); } #region Business Rules /// /// Specication to test if the value of the candidate is valid /// internal class IgnoreOrLocationData : PredicateSpecification { public IgnoreOrLocationData() : base(x => x == DataSourceTypeSensors.Ignore || x == DataSourceTypeSensors.LocationData) { Description = "Only Ignore or LocationData value allowed for this property."; } } /// /// Specication to test if the value of the candidate is valid /// internal class SensorOrLocationData : PredicateSpecification { public SensorOrLocationData() : base(x => x == DataSourceTypeSensors.Sensor || x == DataSourceTypeSensors.LocationData) { Description = "Only Sensor or LocationData value allowed for this property."; } } #endregion }