// Copyright (C) Stichting Deltares 2024. 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.Data; using System.Linq; using Deltares.DamEngine.Calculators.Properties; using Deltares.DamEngine.Data.General; using Deltares.DamEngine.Data.General.PlLines; using Deltares.DamEngine.Data.Geometry; using Deltares.DamEngine.Data.Geotechnics; using Deltares.DamEngine.Data.Standard; namespace Deltares.DamEngine.Calculators.KernelWrappers.MacroStabilityCommon; public static class PlLinesToWaternetConverter { private enum LayerType { BottomAquifer, TopLayerInBetweenAquiferCluster, BottomLayerInBetweenAquiferCluster } private const double epsilon = 1e-9; private const string waternetLine1Name = "Waternet line phreatic line"; private const string waternetLine2Name = "Penetration zone lower aquifer"; private const string waternetLine3Name = "Waternet line lower aquifer"; private const string waternetLine4Name = "Waternet area in-between aquifer"; private const string headLine1Name = "Phreatic line (PL 1)"; private const string headLine2Name = "Head line 2 (PL 2)"; private const string headLine3Name = "Head line 3 (PL 3)"; private const string headLine4Name = "Head line 4 (PL 4)"; /// /// Creates a based on a 1D profile by assigning the to waternet lines. /// /// The pl lines. /// The 1D soil profile. /// Length of the penetration. /// Left side of the 2D profile. /// Right side of the 2D profile. /// public static Waternet CreateWaternetBasedOnPlLines(PlLines plLines, SoilProfile1D soilProfile1D, double penetrationLength, double xLeft, double xRight) { ThrowWhenPlLinesIsNull(plLines); ThrowWhenSoilProfileIsNull(soilProfile1D); var waternet = new Waternet(); PlLine plLine = plLines.Lines[PlLineType.Pl1]; var headLine = CreateLine(plLine, headLine1Name); if (plLine != null && !IsBelowSoilProfile(soilProfile1D, plLine)) { waternet.PhreaticLine = CreateLine(plLine, headLine1Name); double level = soilProfile1D.GetLayerAt(headLine.GetMinZ()).BottomLevel; WaternetLine waternetLine = CreateWaternetLine(level, xLeft, xRight); waternetLine.Name = waternetLine1Name; waternetLine.HeadLine = headLine; waternetLine.HeadLine.SyncCalcPoints(); waternet.WaternetLineList.Add(waternetLine); } plLine = plLines.Lines[PlLineType.Pl2]; headLine = CreateLine(plLine, headLine2Name); if (headLine != null && !IsBelowSoilProfile(soilProfile1D, plLine)) { waternet.HeadLineList.Add(headLine); if (soilProfile1D.BottomAquiferLayer != null) { double level = soilProfile1D.BottomAquiferLayer.TopLevel + penetrationLength; WaternetLine waternetLine = CreateWaternetLine(level, xLeft, xRight); waternetLine.Name = waternetLine2Name; waternetLine.HeadLine = headLine; waternet.WaternetLineList.Add(waternetLine); } } plLine = plLines.Lines[PlLineType.Pl3]; headLine = CreateLine(plLine, headLine3Name); if (headLine != null && !IsBelowSoilProfile(soilProfile1D, plLine)) { waternet.HeadLineList.Add(headLine); if (soilProfile1D.BottomAquiferLayer != null) { double level = soilProfile1D.BottomAquiferLayer.TopLevel; WaternetLine waternetLine = CreateWaternetLine(level, xLeft, xRight); waternetLine.Name = waternetLine3Name; waternetLine.HeadLine = headLine; waternet.WaternetLineList.Add(waternetLine); } } CreateWaternetLinesForInBetweenAquifers(plLines, soilProfile1D, xLeft, xRight, waternet); AdjustWaternetLineOfPhreaticLineWhenCoincidingWithOtherWaternetLines(waternet); return waternet; } /// /// Creates a based on a 2D profile by assigning the to waternet lines. /// /// The to convert. /// The to convert the with. /// The penetration length. /// A . /// Thrown when or /// is null. public static Waternet CreateWaternetBasedOnPlLines(PlLines plLines, SoilProfile2D soilProfile, double penetrationLength) { ThrowWhenPlLinesIsNull(plLines); ThrowWhenSoilProfileIsNull(soilProfile); double[] xCoordinates = DetermineAllXCoordinatesOfSoilProfile(soilProfile); Point2D[] bottomAquiferCoordinates = DetermineLayerBoundaryCoordinates(LayerType.BottomAquifer, xCoordinates, soilProfile).ToArray(); var waternet = new Waternet(); PlLine plLine = plLines.Lines[PlLineType.Pl1]; if (plLine != null) { var headLine = CreateLine(plLine, headLine1Name); waternet.PhreaticLine = CreateLine(plLine, headLine1Name); WaternetLine waternetLine = CreateWaternetLineForPhreaticLine(soilProfile, plLine); waternetLine.Name = waternetLine1Name; waternetLine.HeadLine = headLine; waternet.WaternetLineList.Add(waternetLine); } plLine = plLines.Lines[PlLineType.Pl2]; if (plLine != null) { var headLine = CreateLine(plLine, headLine2Name); waternet.HeadLineList.Add(headLine); if (bottomAquiferCoordinates.Any()) { WaternetLine waternetLine = CreateWaternetLine(bottomAquiferCoordinates, penetrationLength); waternetLine.Name = waternetLine2Name; waternetLine.HeadLine = headLine; waternet.WaternetLineList.Add(waternetLine); } } plLine = plLines.Lines[PlLineType.Pl3]; if (plLine != null) { var headLine = CreateLine(plLine, headLine3Name); waternet.HeadLineList.Add(headLine); if (bottomAquiferCoordinates.Any()) { WaternetLine waternetLine = CreateWaternetLine(bottomAquiferCoordinates); waternetLine.HeadLine = headLine; waternetLine.Name = waternetLine3Name; waternet.WaternetLineList.Add(waternetLine); } } CreateWaternetLinesForInBetweenAquifers(waternet, soilProfile, plLines.Lines[PlLineType.Pl4]); AdjustWaternetLineOfPhreaticLineWhenCoincidingWithOtherWaternetLines(waternet); return waternet; } /// /// Determine all the xCoordinates to make cross-sections. /// /// The soil profile 2D. /// All the xCoordinates of the soil profile 2D. private static double[] DetermineAllXCoordinatesOfSoilProfile(SoilProfile2D soilProfile) { IEnumerable points = soilProfile.Surfaces.SelectMany(surf => surf.GeometrySurface.OuterLoop.CalcPoints); double[] xCoordinates = points.Select(point => point.X).OrderBy(x => x).Distinct().ToArray(); return xCoordinates; } private static void CreateWaternetLinesForInBetweenAquifers(PlLines plLines, SoilProfile1D soilProfile1D, double xLeft, double xRight, Waternet waternet) { PlLine plLine = plLines.Lines[PlLineType.Pl4]; var headLine = CreateLine(plLine, headLine4Name); if (headLine != null && !IsBelowSoilProfile(soilProfile1D, plLine)) { waternet.HeadLineList.Add(headLine); if (soilProfile1D.InBetweenAquiferClusters != null) { foreach ((SoilLayer1D, SoilLayer1D) inBetweenAquiferCluster in soilProfile1D.InBetweenAquiferClusters.Where(layer => layer is { Item1: not null, Item2: not null })) { double levelTop = inBetweenAquiferCluster.Item1.TopLevel; double levelBottom = inBetweenAquiferCluster.Item2.BottomLevel; WaternetLine waternetLine = CreateWaternetLine(levelTop, levelBottom, xLeft, xRight); waternetLine.Name = waternetLine4Name; waternetLine.HeadLine = headLine; waternet.WaternetLineList.Add(waternetLine); } } } } private static void CreateWaternetLinesForInBetweenAquifers(Waternet waternet, SoilProfile2D soilProfile, PlLine plLine) { if (plLine == null) { return; } var headLine = CreateLine(plLine, headLine4Name); waternet.HeadLineList.Add(headLine); double[] xCoordinates = DetermineAllXCoordinatesOfSoilProfile(soilProfile); int inBetweenAquiferCount = DetermineInBetweenAquiferClusterCount(soilProfile, xCoordinates); if (inBetweenAquiferCount == 0) { return; } for (var i = 0; i < inBetweenAquiferCount; i++) { Point2D[] inBetweenAquiferUpperCoordinates = DetermineLayerBoundaryCoordinates(LayerType.TopLayerInBetweenAquiferCluster, xCoordinates, soilProfile, i).ToArray(); Point2D[] inBetweenAquiferLowerCoordinates = DetermineLayerBoundaryCoordinates(LayerType.BottomLayerInBetweenAquiferCluster, xCoordinates, soilProfile, i).ToArray(); if (inBetweenAquiferUpperCoordinates.Any() && inBetweenAquiferLowerCoordinates.Any()) { WaternetLine waternetLine = CreateWaternetLine(inBetweenAquiferUpperCoordinates, inBetweenAquiferLowerCoordinates); waternetLine.HeadLine = headLine; waternetLine.Name = waternetLine4Name; waternet.WaternetLineList.Add(waternetLine); } } } private static int DetermineInBetweenAquiferClusterCount(SoilProfile2D soilProfile, double[] xCoordinates) { var currentCount = 0; var previousCount = 0; for (var i = 0; i < xCoordinates.Length; i++) { SoilProfile1D crossSection = soilProfile.GetSoilProfile1D(xCoordinates[i]); currentCount = crossSection.InBetweenAquiferClusters?.Count ?? 0; if (i > 0 && currentCount != previousCount) { return 0; } previousCount = currentCount; } return currentCount; } private static TLineType CreateLine(PlLine plLine, string name) where TLineType : GeometryPointString, new() { if (plLine == null || !plLine.Points.Any()) { return null; } var line = new TLineType(); line.Points.AddRange(plLine.Points); line.SyncCalcPoints(); line.Name = name; return line; } internal static WaternetLine CreateWaternetLine(double level, double xLeft, double xRight) { var waternetLine = new WaternetLine(); waternetLine.Points.Add(new GeometryPoint(xLeft, level)); waternetLine.Points.Add(new GeometryPoint(xRight, level)); waternetLine.SyncCalcPoints(); return waternetLine; } private static WaternetLine CreateWaternetLine(double levelTop, double levelBottom, double xLeft, double xRight) { var waternetLine = new WaternetLine(); waternetLine.Points.Add(new GeometryPoint(xLeft, levelTop)); waternetLine.Points.Add(new GeometryPoint(xRight, levelTop)); waternetLine.Points.Add(new GeometryPoint(xRight, levelBottom)); waternetLine.Points.Add(new GeometryPoint(xLeft, levelBottom)); waternetLine.Points.Add(new GeometryPoint(xLeft, levelTop)); waternetLine.SyncCalcPoints(); return waternetLine; } private static WaternetLine CreateWaternetLine(IEnumerable coordinates) { return CreateWaternetLine(coordinates, 0); } private static WaternetLine CreateWaternetLine(IEnumerable coordinates, double zOffSet) { var line = new WaternetLine(); foreach (Point2D coordinate in coordinates) { var point = new GeometryPoint(coordinate.X, coordinate.Z + zOffSet); line.Points.Add(point); } line.SyncCalcPoints(); LineHelper.RemoveDuplicatedPoints(line.CalcPoints, epsilon); line.RemoveUnnecessaryPoints(); line.SyncPoints(); return line; } private static WaternetLine CreateWaternetLine(IEnumerable upperCoordinates, IEnumerable lowerCoordinates) { var line = new WaternetLine(); List upperCoordList = upperCoordinates.ToList(); upperCoordList.Sort(); GeometryPoint point; foreach (Point2D upperCoord in upperCoordList) { point = new GeometryPoint(upperCoord.X, upperCoord.Z); line.Points.Add(point); } List lowerCoordList = lowerCoordinates.ToList(); lowerCoordList.Sort(); for (int i = lowerCoordList.Count - 1; i >= 0; i--) { point = new GeometryPoint(lowerCoordList[i].X, lowerCoordList[i].Z); line.Points.Add(point); } point = new GeometryPoint(upperCoordList[0].X, upperCoordList[0].Z); line.Points.Add(point); line.SyncCalcPoints(); line.RemoveUnnecessaryPoints(); line.SyncPoints(); return line; } private static IEnumerable DetermineLayerBoundaryCoordinates(LayerType layerType, double[] xCoordinates, SoilProfile2D soilProfile, int indexInBetweenAquifer = -1) { SoilLayer1D previousAquiferLayer = null; var coordinates = new List(); var xCoordinatesAll = new List(); foreach (double xCoordinate in xCoordinates) { if (HasAquiferAVerticalPartAtGivenX(layerType, xCoordinate, soilProfile, indexInBetweenAquifer)) { xCoordinatesAll.Add(xCoordinate - epsilon); xCoordinatesAll.Add(xCoordinate + epsilon); } else { xCoordinatesAll.Add(xCoordinate); } } foreach (double xCoordinate in xCoordinatesAll) { SoilProfile1D crossSection = soilProfile.GetSoilProfile1D(xCoordinate); // Determine if the layer is in range of the previous layer // If not, return empty coordinates, because the layer is interrupted SoilLayer1D currentAquifer = GetSoilLayer1D(layerType, crossSection, indexInBetweenAquifer); if (previousAquiferLayer != null && currentAquifer != null && !AreHorizontallyConnected(previousAquiferLayer, currentAquifer)) { return Enumerable.Empty(); } if (currentAquifer != null) { previousAquiferLayer = currentAquifer; coordinates.Add(layerType is LayerType.BottomAquifer or LayerType.TopLayerInBetweenAquiferCluster ? new Point2D(xCoordinate, currentAquifer.TopLevel) : new Point2D(xCoordinate, currentAquifer.BottomLevel)); } } // Perform a short validation that the coordinates are fully defined from the beginning to the end // of the profile. If not, the layer is not continuous. if (!IsLayerBoundaryContinuous(coordinates, xCoordinates.First(), xCoordinates.Last())) { return Enumerable.Empty(); } return coordinates; } private static bool IsLayerBoundaryContinuous(List boundaryPoints, double leftGeometryBoundary, double rightGeometryBoundary) { return boundaryPoints.Any() && (Math.Abs(boundaryPoints.First().X - leftGeometryBoundary) < epsilon) && Math.Abs(boundaryPoints.Last().X - rightGeometryBoundary) < epsilon; } private static SoilLayer1D GetSoilLayer1D(LayerType layerType, SoilProfile1D soilProfile1D, int indexInBetweenAquifer) { switch (layerType) { case LayerType.BottomAquifer: return soilProfile1D.BottomAquiferLayer; case LayerType.TopLayerInBetweenAquiferCluster: return soilProfile1D.InBetweenAquiferClusters[indexInBetweenAquifer].Item1; case LayerType.BottomLayerInBetweenAquiferCluster: return soilProfile1D.InBetweenAquiferClusters[indexInBetweenAquifer].Item2; } return null; } /// /// Create the waternet line associated to the phreatic line (PL1). /// For the waternet type "Dam Standard", this line lies on the bottom of the soil layers “in which the phreatic plane lies”. /// Where PL1 lies above the ground level, the line lies on the ground level. /// As a consequence, the line can contain vertical "jumps" when layers have vertical parts. /// /// /// /// private static WaternetLine CreateWaternetLineForPhreaticLine(SoilProfile2D soilProfile2D, PlLine plLine) { var waternetLine = new WaternetLine(); var curves = new List(); foreach (SoilLayer2D surface in soilProfile2D.Surfaces) { GeometryLoop outerLoop = surface.GeometrySurface.OuterLoop; // Add the first point to get a closed GeometryPointString outerLoop.CalcPoints.Add(new Point2D(outerLoop.CalcPoints[0].X, outerLoop.CalcPoints[0].Z)); var plLineInGeometryPoint = new GeometryPointString(); foreach (PlLinePoint point in plLine.Points) { plLineInGeometryPoint.Points.Add(new GeometryPoint(point.X, point.Z)); plLineInGeometryPoint.SyncCalcPoints(); } List intersectionPoints = outerLoop.IntersectionXzPointsWithGeometryString(plLineInGeometryPoint); // Only the layers that are below and do not intersect the phreatic line are kept. if ((intersectionPoints.Count == 0) && (outerLoop[0].Z < plLineInGeometryPoint.GetZatX(outerLoop[0].X))) { var geometrySurface = new GeometrySurface(outerLoop); GeometryPointString top = geometrySurface.DetermineTopGeometrySurface(); top.SyncPoints(); top.SortPointsByXAscending(); curves.Add(top); } } // The waternet line is the polyline formed with the highest lines connected to each other. List lines = DivideCurvesIntoLines(curves); IEnumerable allPoints = curves.SelectMany(curve => curve.CalcPoints); double[] xCoordinates = allPoints.Select(point => point.X).OrderBy(x => x).Distinct().ToArray(); List splitLines = SplitLinesAtXCoordinates(xCoordinates, lines); for (var i = 0; i < xCoordinates.Length - 1; i++) { List relevantLines = splitLines.FindAll(line => Math.Abs(line.BeginPoint.X - xCoordinates[i]) < epsilon && Math.Abs(line.BeginPoint.X - line.EndPoint.X) > 0); double max = relevantLines.Select(line => line.BeginPoint.Z).Max(); Line relevantLine = relevantLines.Find(line => Math.Abs(line.BeginPoint.Z - max) < epsilon); waternetLine.Points.Add(new GeometryPoint(relevantLine.BeginPoint.X, relevantLine.BeginPoint.Z)); waternetLine.Points.Add(new GeometryPoint(relevantLine.EndPoint.X, relevantLine.EndPoint.Z)); } waternetLine.SyncCalcPoints(); LineHelper.RemoveDuplicatedPoints(waternetLine.CalcPoints, epsilon); waternetLine.RemoveUnnecessaryPoints(); waternetLine.SyncPoints(); return waternetLine; } private static bool HasAquiferAVerticalPartAtGivenX(LayerType layerType, double xCoordinate, SoilProfile2D soilProfile, int indexInBetweenAquifer) { SoilProfile1D crossSectionLeft = soilProfile.GetSoilProfile1D(xCoordinate - epsilon); SoilProfile1D crossSectionRight = soilProfile.GetSoilProfile1D(xCoordinate + epsilon); if (crossSectionLeft != null && crossSectionRight != null) { SoilLayer1D currentAquiferLeft = GetSoilLayer1D(layerType, crossSectionLeft, indexInBetweenAquifer); SoilLayer1D currentAquiferRight = GetSoilLayer1D(layerType, crossSectionRight, indexInBetweenAquifer); if (currentAquiferLeft != null && currentAquiferRight != null) { if (layerType is LayerType.BottomAquifer or LayerType.TopLayerInBetweenAquiferCluster) { return Math.Abs(currentAquiferLeft.TopLevel - currentAquiferRight.TopLevel) > GeometryConstants.Accuracy; } return Math.Abs(currentAquiferLeft.BottomLevel - currentAquiferRight.BottomLevel) > GeometryConstants.Accuracy; } } return false; } private static bool AreHorizontallyConnected(SoilLayer1D leftSoilLayer, SoilLayer1D rightSoilLayer) { // The layers are identical if (ReferenceEquals(leftSoilLayer, rightSoilLayer)) { return true; } // Left soil layer envelopes whole right soil layer if (leftSoilLayer.BottomLevel <= rightSoilLayer.BottomLevel && leftSoilLayer.TopLevel >= rightSoilLayer.TopLevel) { return true; } // Right soil layer envelopes whole left soil layer if (rightSoilLayer.BottomLevel <= leftSoilLayer.BottomLevel && rightSoilLayer.TopLevel >= leftSoilLayer.TopLevel) { return true; } return (rightSoilLayer.TopLevel <= leftSoilLayer.TopLevel && rightSoilLayer.TopLevel >= leftSoilLayer.BottomLevel) // Top level lies inbetween the left soil layer || (rightSoilLayer.BottomLevel >= leftSoilLayer.BottomLevel && rightSoilLayer.BottomLevel <= leftSoilLayer.TopLevel); // Bottom level lies inbetween the left soil layer } private static bool IsBelowSoilProfile(SoilProfile1D soilProfile, PlLine line) { double bottomSoilProfileLevel = soilProfile.BottomLevel; return line.Points.Any(point => point.Z <= bottomSoilProfileLevel); } /// /// Throws when the soil profile is not assigned. /// /// The soil profile. /// private static void ThrowWhenSoilProfileIsNull(SoilProfile soilProfile) { if (soilProfile == null) { throw new NoNullAllowedException(Resources.PlLinesToWaternetConverter_NoSoilProfileDefined); } } /// /// Throws when the pl lines object is not assigned. /// /// The pl lines. /// private static void ThrowWhenPlLinesIsNull(PlLines plLines) { if (plLines == null) { throw new NoNullAllowedException(Resources.PlLinesToWaternetConverter_NoPlLinesDefined); } } /// /// Waternet lines can't coincide otherwise the program does not know which headline to use. /// That's why when the waternet line of the phreatic line coincides with the waternet line of either PL2, PL3 or PL4, /// an adjustment of the waternet line of the phreatic line is performed by moving it 1 mm upwards. /// /// The waternet. private static void AdjustWaternetLineOfPhreaticLineWhenCoincidingWithOtherWaternetLines(Waternet waternet) { const double minimumDistance = 0.001; WaternetLine waternetLine1 = waternet.WaternetLineList.Find(w => w.Name == waternetLine1Name); foreach (GeometryPoint waternetLine1Point in from waternetLineOther in waternet.WaternetLineList.FindAll(w => w.Name != waternetLine1Name) from waternetLine1Point in waternetLine1.Points where waternetLineOther.Points.Any(point => Math.Abs(point.Z - waternetLine1Point.Z) < epsilon) select waternetLine1Point) { waternetLine1Point.Z += minimumDistance; } } private static List SplitLinesAtXCoordinates(double[] xCoordinates, List lines) { var splitLines = new List(); foreach (Line line in lines) { List xCoordinatesToBeAdded = xCoordinates.Where(xCoordinate => line.BeginPoint.X.IsLessThan(xCoordinate) && xCoordinate.IsLessThan(line.EndPoint.X)).ToList(); if (xCoordinatesToBeAdded.Count > 0) { SplitLineAtXCoordinate(xCoordinatesToBeAdded, line, splitLines); } else { splitLines.Add(line); } } return splitLines; } private static void SplitLineAtXCoordinate(List xCoordinatesToBeAdded, Line line, List splitLines) { for (var i = 0; i < xCoordinatesToBeAdded.Count; i++) { var point1 = new Point2D(xCoordinatesToBeAdded[i], Math.Max(line.BeginPoint.Z, line.EndPoint.Z) + 1); var point2 = new Point2D(xCoordinatesToBeAdded[i], Math.Min(line.BeginPoint.Z, line.EndPoint.Z) - 1); double zCoordinate = line.GetIntersectPointXz(new Line(point1, point2)).Z; var middlePoint = new Point2D(xCoordinatesToBeAdded[i], zCoordinate); Point2D beginPoint = i == 0 ? line.BeginPoint : middlePoint; Point2D endPoint = i == xCoordinatesToBeAdded.Count - 1 ? line.EndPoint : middlePoint; splitLines.Add(new Line(beginPoint, middlePoint)); splitLines.Add(new Line(middlePoint, endPoint)); } } private static List DivideCurvesIntoLines(List curves) { var lines = new List(); foreach (GeometryPointString curve in curves) { for (var i = 0; i < curve.Points.Count - 1; i++) { var line = new Line { BeginPoint = new Point2D(curve.Points[i].X, curve.Points[i].Z), EndPoint = new Point2D(curve.Points[i + 1].X, curve.Points[i + 1].Z) }; lines.Add(line); } } return lines; } }