// Copyright (C) Stichting Deltares 2025. All rights reserved.
//
// This file is part of the Dam Engine.
//
// The Dam Engine is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero 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.Generic;
using System.Linq;
using Deltares.DamEngine.Data.Geometry;
using Deltares.DamEngine.Data.Standard;
using Deltares.DamEngine.Data.Standard.Language;
using Deltares.DamEngine.Data.Standard.Validation;
namespace Deltares.DamEngine.Data.Geotechnics;
///
/// Interface for providing the 1D Soil Profile
///
public interface ISoilProfileProvider
{
///
/// Gets the soil profile.
///
///
/// The soil profile.
///
SoilProfile1D SoilProfile { get; }
}
///
/// 1D Soil Profile Object
///
public class SoilProfile1D : SoilProfile
{
private const double defaultBottomLayerHeight = 20.0;
private readonly DelegatedList layers = new DelegatedList();
private double bottomLevel = double.NaN;
///
/// Default constructor
///
public SoilProfile1D()
{
base.Name = LocalizationManager.GetTranslatedText(this, "DefaultNameSoilProfile1D");
layers.AddMethod = AddLayer;
}
///
/// Constructor to construct simple soilprofile
///
///
///
///
public SoilProfile1D(double topLevel, double bottomLevel, Soil soil)
: this()
{
this.bottomLevel = bottomLevel;
layers.Add(new SoilLayer1D(soil, topLevel));
}
///
/// Gets the soil layer collection for this profile
///
public IList Layers
{
get
{
return layers;
}
}
///
/// Gets the number of layers.
///
///
/// The layer count.
///
public int LayerCount
{
get
{
return layers.Count;
}
}
///
/// Gets or sets the bottom level.
///
///
/// The bottom level.
///
public double BottomLevel
{
get
{
if (double.IsNaN(bottomLevel) && layers.Count > 0)
{
bottomLevel = Layers.Last().TopLevel - defaultBottomLayerHeight;
}
return bottomLevel;
}
set
{
if (double.IsNaN(bottomLevel) || (Math.Abs(value - bottomLevel) > GeometryConstants.Accuracy))
{
bottomLevel = value;
}
}
}
///
/// Gets or sets the top level.
///
///
/// The top level.
///
public double TopLevel
{
get
{
return layers.Any() ? layers.First().TopLevel : BottomLevel;
}
}
///
/// Gets the infiltration layer.
///
///
/// The infiltration layer.
///
public SoilLayer1D InfiltrationLayer { get; }
///
/// Gets the highest aquifer layer of the deepest cluster of aquifers
///
///
/// The highest aquifer layer in the deepest cluster of aquifers
///
public SoilLayer1D BottomAquiferLayer
{
get
{
IList sortedLayers = Layers.OrderBy(l => l.TopLevel).ToList();
SoilLayer1D aquiferLayer = DeepestAquiferLayer;
int aquiferIndex = sortedLayers.IndexOf(DeepestAquiferLayer);
// aquifer may consist of more than 1 connected (aquifer) layers
// Search all layers above the deepest aquifer to find top aquifer layer
if (aquiferIndex >= 0)
{
for (int layerIndex = aquiferIndex + 1; layerIndex < sortedLayers.Count; layerIndex++)
{
SoilLayer1D layer = sortedLayers[layerIndex];
if (IsAquiferLayer(layer))
{
aquiferLayer = layer;
}
else
{
break;
}
}
}
return aquiferLayer;
}
}
///
/// Gets the deepest aquifer layer
///
///
/// The deepest aquifer layer
///
public SoilLayer1D DeepestAquiferLayer
{
get
{
IList sortedLayers = Layers.OrderBy(l => l.TopLevel).ToList();
SoilLayer1D aquiferLayer = null;
for (var layerIndex = 0; layerIndex < sortedLayers.Count; layerIndex++)
{
SoilLayer1D layer = sortedLayers[layerIndex];
if (IsAquiferLayer(layer))
{
aquiferLayer = layer;
break;
}
}
return aquiferLayer;
}
}
///
/// Gets the list of all clusters of in-between aquifers:
/// Item1 is the top aquifer and Item2 the bottom aquifer of the cluster.
/// For a cluster of 1 layer, Item1 and Item2 are equal.
/// The top layer of a cluster of in-between aquifer can't be the highest layer of the soil profile.
///
///
/// The list of all clusters of in-between aquifers.
///
public IList<(SoilLayer1D, SoilLayer1D)> GetInBetweenAquiferClusters => InBetweenAquiferClusters();
///
/// Gets the highest aquifer in the highest cluster of in-between aquifers.
/// The top layer of a cluster of in-between aquifer can't be the highest layer of the soil profile.
///
///
/// The highest aquifer in the highest in-between cluster of aquifers
///
public SoilLayer1D InBetweenAquiferLayer
{
get
{
return GetInBetweenAquiferClusters.Count == 0 ? null : GetInBetweenAquiferClusters.First().Item1;
}
}
///
/// Assigns the specified profile.
///
/// The profile.
public void Assign(SoilProfile1D profile)
{
Assign((SoilProfile) profile);
Layers.Clear();
foreach (SoilLayer1D layer in profile.Layers)
{
Layers.Add((SoilLayer1D) layer.Clone());
}
BottomLevel = profile.BottomLevel;
}
///
/// Creates a new object that is a copy of the current instance.
///
///
/// A new object that is a copy of this instance.
///
public object Clone()
{
var cloneSoilProfile = new SoilProfile1D();
cloneSoilProfile.Assign(this);
return cloneSoilProfile;
}
///
/// Gets layer with the specified name.
///
/// The name.
///
public SoilLayer1D GetLayerWithName(string name)
{
return layers.FirstOrDefault(layer =>
{
if (layer.Name != null)
{
return layer.Name.Equals(name);
}
return false;
});
}
///
/// Checks weather all layer names are unique.
///
///
/// true if all layer names are unique; otherwise, false.
///
public bool AreAllLayerNamesUnique()
{
int layersSize = Layers.Count;
for (var i = 0; i < layersSize; ++i)
{
string id1 = layers[i].Name;
if (id1 == null)
{
return false;
}
for (int j = i + 1; j < layersSize; ++j)
{
if (id1.Equals(Layers[j].Name))
{
return false;
}
}
}
return true;
}
///
/// Gets the first unused unique layer name.
///
/// new unique layer name
public string GetNewUniqueLayerName()
{
string newName;
var i = 0;
SoilLayer1D soilLayer;
do
{
newName = "L" + i++;
soilLayer = GetLayerWithName(newName);
} while (soilLayer != null);
return newName;
}
///
/// Gets (calculates) the height for a given layer in the profile
///
/// The layer to process
/// The height
public double GetLayerHeight(SoilLayer1D soilLayer)
{
int layerIndex = layers.IndexOf(soilLayer);
SoilLayer1D soilLayerBelow = (layerIndex < layers.Count - 1) ? layers[layerIndex + 1] : null;
double levelBelow = (soilLayerBelow != null) ? soilLayerBelow.TopLevel : BottomLevel;
return soilLayer.TopLevel - levelBelow;
}
///
/// Make sure the last layer has a height
///
public void EnsureLastLayerHasHeight()
{
SoilLayer1D bottomLayer = Layers.Last();
if (bottomLayer.Height.IsZero())
{
BottomLevel -= defaultBottomLayerHeight;
}
}
///
/// Validates this instance (using validator mechanism).
///
///
[Validate]
public ValidationResult[] Validate()
{
SoilLayer1D erroneousLayer;
if (LayerCount == 0)
{
string error = String.Format(LocalizationManager.GetTranslatedText(this, "SoilProfileWithoutLayers"), Name);
return new[]
{
new ValidationResult(ValidationResultType.Error,
error, "", this)
};
}
if (!IsStrictlyDescending(out erroneousLayer))
{
string error = String.Format(LocalizationManager.GetTranslatedText(this, "SoilProfileLayersNotDescending"),
Name, erroneousLayer.Name);
return new[]
{
new ValidationResult(ValidationResultType.Error,
error, "", this)
};
}
if (HasInvalidThicknessLayers(out erroneousLayer))
{
string error = String.Format(LocalizationManager.GetTranslatedText(this, "SoilProfileInvalidLayerThickness"),
Name, erroneousLayer.Name);
return new[]
{
new ValidationResult(ValidationResultType.Error,
error, "", this)
};
}
if (HasLayersWithoutSoil(out erroneousLayer))
{
string error = String.Format(LocalizationManager.GetTranslatedText(this, "SoilProfileLayerWithoutSoil"),
Name, erroneousLayer.Name);
return new[]
{
new ValidationResult(ValidationResultType.Error,
error, "", this)
};
}
return new ValidationResult[0];
}
///
/// Gets the aquifer layers.
///
/// list of Aquifer layers
public IList GetAquiferLayers()
{
return Layers.Where(IsAquiferLayer).OrderBy(l => l.TopLevel).ToList();
}
///
/// Gets the bottom level.
///
/// The soil layer.
/// Bottom level
public double GetBottomLevel(SoilLayer1D soilLayer)
{
SoilLayer1D layerBelow = GetLayerBelow(soilLayer);
return layerBelow != null ? layerBelow.TopLevel : BottomLevel;
}
///
/// Gets the layer at a given z coordinate.
///
/// The z.
/// the found layer
public SoilLayer1D GetLayerAt(double z)
{
return Layers.FirstOrDefault(soilLayer => soilLayer.BottomLevel < z && soilLayer.TopLevel >= z);
}
///
/// Gets the layer below the given layer.
///
/// The given layer.
/// The found layer
public SoilLayer1D GetLayerBelow(SoilLayer1D layer)
{
return GetLayerByInsetOffset(layer, 1);
}
///
/// Gets the layer index of the given layer.
///
/// The layer.
/// The index, -1 if not found
public int GetLayerIndexAt(SoilLayer1D layer)
{
if (layer == null)
{
return -1;
}
for (var i = 0; i < layers.Count; ++i)
{
if (layers[i] == layer)
{
return i;
}
}
return -1;
}
///
/// Gets the highest aquifer.
///
///
public SoilLayer1D GetHighestAquifer()
{
SoilLayer1D layer;
if (InBetweenAquiferLayer != null)
{
layer = InBetweenAquiferLayer;
}
else
{
layer = BottomAquiferLayer;
}
return layer;
}
///
/// Gets the lowest layer of the highest cluster of aquifers.
///
///
public SoilLayer1D GetLowestLayerOfHighestAquiferCluster()
{
SoilLayer1D layer;
if (InBetweenAquiferLayer != null)
{
layer = GetInBetweenAquiferClusters.Count == 0 ? null : GetInBetweenAquiferClusters.First().Item2;
}
else
{
layer = DeepestAquiferLayer;
}
return layer;
}
///
/// Returns a that represents this instance.
///
///
/// A that represents this instance.
///
public override string ToString()
{
return Name;
}
private IList<(SoilLayer1D, SoilLayer1D)> InBetweenAquiferClusters()
{
// At least 4 layers are needed (aquitard, aquifer, aquitard, aquifer) to get one in-between aquifer
if (LayerCount <= 3)
{
return new List<(SoilLayer1D, SoilLayer1D)>();
}
IList sortedLayers = Layers.OrderByDescending(l => l.TopLevel).ToList();
IList topLayers = DetermineTopLayersOfInBetweenAquiferClusters(sortedLayers);
if (topLayers.Count <= 0)
{
return new List<(SoilLayer1D, SoilLayer1D)>();
}
IList bottomLayers = DetermineBottomLayersOfInBetweenAquiferClusters(sortedLayers, layers.IndexOf(topLayers.First()));
return topLayers.Count != bottomLayers.Count ? null : topLayers.Select((topLayer, i) => new ValueTuple(topLayer, bottomLayers[i])).ToList();
}
private List DetermineTopLayersOfInBetweenAquiferClusters(IList sortedLayers)
{
List topLayers = [];
for (var layerIndex = 1; layerIndex < sortedLayers.Count - 1; layerIndex++)
{
SoilLayer1D previousLayer = sortedLayers[layerIndex - 1];
SoilLayer1D layer = sortedLayers[layerIndex];
if (IsAquiferLayer(layer) && !IsAquiferLayer(previousLayer) && layerIndex < sortedLayers.IndexOf(BottomAquiferLayer))
{
topLayers.Add(layer);
}
}
return topLayers;
}
private List DetermineBottomLayersOfInBetweenAquiferClusters(IList sortedLayers, int indexStart)
{
List bottomLayers = [];
for (int layerIndex = indexStart; layerIndex < sortedLayers.Count - 1; layerIndex++)
{
SoilLayer1D layer = sortedLayers[layerIndex];
SoilLayer1D nextLayer = sortedLayers[layerIndex + 1];
if (IsAquiferLayer(layer) && !IsAquiferLayer(nextLayer) && layerIndex < sortedLayers.IndexOf(BottomAquiferLayer))
{
bottomLayers.Add(layer);
}
}
return bottomLayers;
}
///
/// Ares the layers ordered descending.
///
///
/// true if all layers are ordered descending; otherwise, false.
///
private bool AreLayersOrderedByDescending()
{
// check for empty list
if (Layers.Count <= 1)
{
return true;
}
SoilLayer1D current = Layers[0];
for (var i = 1; i < Layers.Count; ++i)
{
SoilLayer1D previous = current;
current = Layers[i];
if (current.TopLevel > previous.TopLevel)
{
return false;
}
}
return true;
}
///
/// Determines whether the layers are strictly ordered in descending order.
/// If erroneous layer is found, false is returned and the layer
/// is stored in the parameter
///
/// The erroneous layer.
/// true if the layers are strictly ordered in descending order; otherwise, false.
private bool IsStrictlyDescending(out SoilLayer1D erroneousLayer)
{
for (var i = 1; i < layers.Count; i++)
{
if (layers[i].TopLevel > layers[i - 1].TopLevel)
{
erroneousLayer = layers[i];
return false;
}
}
erroneousLayer = null;
return true;
}
///
/// Determines whether there are layers with invalid thickness.
///
/// The erroneous layer.
/// True when a layer with invalid thickness exists
private bool HasInvalidThicknessLayers(out SoilLayer1D erroneousLayer)
{
for (var i = 1; i < layers.Count; i++)
{
if (layers[i].TopLevel >= layers[i - 1].TopLevel)
{
erroneousLayer = layers[i];
return true;
}
}
// check bottom layer using bottom of profile
if (layers.Count > 0 && layers[layers.Count - 1].TopLevel <= BottomLevel)
{
erroneousLayer = layers[layers.Count - 1];
return true;
}
erroneousLayer = null;
return false;
}
///
/// Determines whether there are layers without soil.
///
/// The erroneous layer.
/// true when a layer without soil is found
private bool HasLayersWithoutSoil(out SoilLayer1D erroneousLayer)
{
for (var i = 0; i < layers.Count; i++)
{
if (layers[i].Soil == null)
{
erroneousLayer = layers[i];
return true;
}
}
erroneousLayer = null;
return false;
}
///
/// Gets the layer by inset offset, seen from the given layer plus the given offset.
///
/// The layer.
/// The offset.
/// the found layer, if no layer found, null
private SoilLayer1D GetLayerByInsetOffset(SoilLayer1D layer, int offset)
{
// only works if list is sorted
if (!AreLayersOrderedByDescending())
{
layers.Sort();
}
int index = GetLayerIndexAt(layer);
// was the layer found?
if (index < 0 || index >= layers.Count)
{
return null;
}
// is there a layer at the specified offset?
int requestedIndex = index + offset;
if (requestedIndex < 0 || requestedIndex >= layers.Count)
{
return null;
}
// return the valid layer
return layers[requestedIndex];
}
private void AddLayer(SoilLayer1D layer)
{
layer.SoilProfile = this;
}
///
/// Determines whether the specified layer is an aquifer layer.
///
/// The layer.
/// true if layer is aquifer layer; otherwise, false.
private static bool IsAquiferLayer(SoilLayer1D layer)
{
return (layer.IsAquifer);
}
}