// 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
}