// Copyright (C) Stichting Deltares 2019. 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.Validation; namespace Deltares.DamEngine.Data.Geotechnics { /// /// /// A SoilSurfaceProfile constructs a SoilProfile2D object by merging an existing SoilProfile1D with a SurfaceLine /// /// Note that actual contruction of 2D based on surface line and 1D happens on setting the properties using dirty flags. /// public class SoilSurfaceProfile : SoilProfile2D { private Soil dikeEmbankmentMaterial; private SoilProfile1D soilProfile; private SoilProfile1D originalSoilProfile1D; private SurfaceLine2 surfaceLine2; /// /// Initializes a new instance of the class. /// public SoilSurfaceProfile() { base.Name = "Soil Surface Profile"; } /// /// Gets or sets the surface line2. /// [Validate] public SurfaceLine2 SurfaceLine2 { get { return surfaceLine2; } set { surfaceLine2 = value; } } /// /// Gets or sets the soil profile. /// /// /// The soil profile. /// [Validate] public SoilProfile1D SoilProfile { get { return originalSoilProfile1D; } set { originalSoilProfile1D = value; } } /// /// Gets or sets the dike material. /// /// /// The dike material. /// public Soil DikeEmbankmentMaterial { get { return dikeEmbankmentMaterial; } set { dikeEmbankmentMaterial = value; } } /// /// Returns a that represents this instance (either Name or SoilProfile1D). /// /// /// A that represents this instance. /// public override string ToString() { if (!string.IsNullOrEmpty(Name)) { return Name; } if (originalSoilProfile1D != null) { return originalSoilProfile1D.ToString(); } return "Soil Surface Profile with null Soil Profile"; } /// /// Converts to soil profile2D. /// /// public SoilProfile2D ConvertToSoilProfile2D() { // Generate the geometry for the new 2D profile Create2DGeometryBasedOn1DProfileAndSurfaceLine(); // Create the actual new 2D profile var soilProfile2D = new SoilProfile2D { Geometry = Geometry }; soilProfile2D.Surfaces.AddRange(Surfaces); soilProfile2D.Name = Name; return soilProfile2D; } /// /// Function to create and update the , /// based on a 1D profile and a surface line. /// private void Create2DGeometryBasedOn1DProfileAndSurfaceLine() { var filteredSoilLayers = FilterSoilLayersToConvert(originalSoilProfile1D, surfaceLine2); var localSurfaceLinePoints = GetLocalSurfaceLinePoints(filteredSoilLayers, surfaceLine2); var layerData = CreateLayerData(filteredSoilLayers, localSurfaceLinePoints); var filteredLayerData = FilterLayerDataWithClosedLoops(layerData); BuildGeometryModel(Geometry, filteredLayerData, originalSoilProfile1D, surfaceLine2, localSurfaceLinePoints); AssignSoilLayer2DProperties(Surfaces, filteredLayerData, Geometry); } private static void BuildGeometryModel(GeometryData geometryData, IEnumerable layers, SoilProfile1D profile, SurfaceLine2 surfaceLine, IEnumerable localSurfaceLinePoints) { if (surfaceLine.Geometry == null || profile == null || profile.Layers.Count == 0 || surfaceLine.Geometry.GetMaxZ() < profile.BottomLevel || surfaceLine.Geometry.Points.Count < 2) { return; } GeometryBounds bounds = surfaceLine.Geometry.GetGeometryBounds(); double minX = bounds.Left; double maxX = bounds.Right; geometryData.Clear(); geometryData.Left = minX; geometryData.Right = maxX; geometryData.Bottom = Math.Min(profile.BottomLevel, surfaceLine.Geometry.GetMinZ() - 1); var localSurfaceLineCurves = GenerateCurves(localSurfaceLinePoints.ToArray()); var pointsToBeAdded = layers.SelectMany(l => l.Points) .Concat(localSurfaceLinePoints); var curvesToBeAdded = layers.SelectMany(l => l.Curves) .Concat(localSurfaceLineCurves); geometryData.NewlyEffectedPoints.AddRange(pointsToBeAdded); geometryData.NewlyEffectedCurves.AddRange(curvesToBeAdded); geometryData.RegenerateGeometry(); } private static void AssignSoilLayer2DProperties(ICollection surfaces, IEnumerable layers, GeometryData geometryData) { surfaces.Clear(); foreach (GeometrySurface surface in geometryData.Surfaces) { GeometryBounds bounds = surface.GetGeometryBounds(); double z = (bounds.Top + bounds.Bottom) * 0.5; // Get the corresponding layer data. This assumption only holds // because each z coordinate contains ONE single layer definition. LayerData layerData = layers.FirstOrDefault(l => l.IsZLocatedInLayer(z)); if (layerData != null) { surfaces.Add(new SoilLayer2D { GeometrySurface = surface, IsAquifer = layerData.IsAquifer, WaterpressureInterpolationModel = layerData.WaterpressureInterpolationModel, Soil = layerData.Soil }); } } } /// /// Gets the collection of to convert based on the input arguments. /// /// The to retrieve the layers for. /// The surface line. /// The collection of to convert. private IEnumerable FilterSoilLayersToConvert(SoilProfile1D profile, SurfaceLine2 surfaceLine) { // Make a copy of the soil profile to prevent modifications of the reference argument. // The clone method does not copy the properties of the WaterPressureInterpolationModel // and the IsAquiferProperties properly, so these need to be set manually var copiedSoilProfile = (SoilProfile1D)profile.Clone(); var soilLayers = copiedSoilProfile.Layers; for (int i = 0; i < soilLayers.Count; i++) { CopyLayerProperties(soilLayers[i], profile.Layers[i]); } double maxZCoordinateSurfaceLine = surfaceLine.Geometry.GetMaxZ(); // Create an artificial top layer based on the dike embankment material in case the top of the highest soil layer // lies below the maximum coordinate of the surface line. double maxZCoordinateLayers = soilLayers.Select(l => l.TopLevel).Max(); if (maxZCoordinateLayers < maxZCoordinateSurfaceLine) { soilLayers.Insert(0, new SoilLayer1D(DikeEmbankmentMaterial, maxZCoordinateSurfaceLine) { WaterpressureInterpolationModel = WaterpressureInterpolationModel.Hydrostatic }); } copiedSoilProfile.EnsureLastLayerHasHeight(); return copiedSoilProfile.Layers.Where(l => !IsLayerAboveSurfaceLine(l, surfaceLine)); } private static void CopyLayerProperties(SoilLayer1D copiedLayer, SoilLayer1D originalLayer) { copiedLayer.IsAquifer = originalLayer.IsAquifer; copiedLayer.WaterpressureInterpolationModel = originalLayer.WaterpressureInterpolationModel; } private static bool IsLayerBelowSurfaceLine(SoilLayer1D soilLayer, IEnumerable surfaceLinePoints) { return surfaceLinePoints.Min(p => p.Z) > soilLayer.TopLevel; } private static bool IsLayerAboveSurfaceLine(SoilLayer1D soilLayer, SurfaceLine2 surfaceLine2) { return soilLayer.BottomLevel > surfaceLine2.Geometry.GetMaxZ(); } private static IEnumerable GetLocalSurfaceLinePoints(IEnumerable soilLayers, SurfaceLine2 surfaceLine) { var xCoordinates = GetXCoordinates(soilLayers, surfaceLine); return xCoordinates.Select(x => new Point2D(x, surfaceLine.Geometry.GetZatX(x))).ToArray(); } /// /// Gets the X coordinates to determine the coordinates for through each layer. /// /// The collection of . /// The . /// A collection of unique x coordinates, in an ascending order. /// This function is necessary, because the algorithm in recognizing /// surfaces requires that curves are overlapping. Curves cannot be a subset (e.g, there is one /// curve definition that span two other curves, because the algorithm will not /// be able to determine a surface from it. private static IEnumerable GetXCoordinates(IEnumerable soilLayers, SurfaceLine2 surfaceLine) { var xCoordinates = new List(surfaceLine.Geometry.CalcPoints.Select(p => p.X)); foreach (SoilLayer1D layer in soilLayers) { xCoordinates.AddRange(GetUniqueXCoordinatesWithIntersections(surfaceLine, layer.TopLevel, layer.BottomLevel)); } return xCoordinates.OrderBy(x => x).Distinct(); } private static IEnumerable GetUniqueXCoordinatesWithIntersections(SurfaceLine2 surfaceLine, double layerTopLevel, double layerBottomLevel) { GeometryPointString surfaceLineGeometry = surfaceLine.Geometry; double minXCoordinate = surfaceLineGeometry.GetMinX(); double maxXCoordinate = surfaceLineGeometry.GetMaxX(); // Create lines to determine intersections var topLeft = new Point2D(minXCoordinate, layerTopLevel); var topRight = new Point2D(maxXCoordinate, layerTopLevel); var topLine = new Line(topLeft, topRight); var bottomLeft = new Point2D(minXCoordinate, layerBottomLevel); var bottomRight = new Point2D(maxXCoordinate, layerBottomLevel); var bottomLine = new Line(bottomLeft, bottomRight); var xCoordinates = surfaceLineGeometry.IntersectionPointsXzWithLineXz(topLine).Select(p => p.X).ToList(); xCoordinates.AddRange(surfaceLineGeometry.IntersectionPointsXzWithLineXz(bottomLine).Select(p => p.X)); return xCoordinates.Distinct(); } private static IEnumerable CreateLayerData(IEnumerable soilLayers, IEnumerable surfaceLinePoints) { var layerData = new List(); foreach (SoilLayer1D soilLayer in soilLayers) { if (IsLayerBelowSurfaceLine(soilLayer, surfaceLinePoints)) { layerData.Add(CreateSurfaceLineWideSoilLayer(soilLayer, surfaceLinePoints)); } else { layerData.AddRange(CreateSoilLayerWithSurfaceLineIntersection(soilLayer, surfaceLinePoints)); } } return layerData; } /// /// Filters layer data that consist of closed loops. /// /// The collection of to filter. /// A collection of that define a closed loop. private static IEnumerable FilterLayerDataWithClosedLoops(IEnumerable layerData) { return layerData.Where(l => l.Curves.Count() > 2); } /// /// Creates a 2D soil layer with points at . /// /// The to create the 2D soil layer for. /// The collection of that defines the surface line. /// A based on the input arguments. private static LayerData CreateSurfaceLineWideSoilLayer(SoilLayer1D soilLayer, IEnumerable surfaceLinePoints) { // A single square definition does not suffice for the area definition. The // curves need to overlap each other or the geometry generator fails to recognize // a surface double layerTopLevel = soilLayer.TopLevel; double layerBottomLevel = soilLayer.BottomLevel; var pointsToConvert = surfaceLinePoints.Select(p => new Point2D(p.X, layerTopLevel)).ToList(); pointsToConvert.AddRange(surfaceLinePoints.Reverse().Select(p => new Point2D(p.X, layerBottomLevel)).ToArray()); var curves = GenerateClosedLoopCurves(pointsToConvert); return new LayerData(pointsToConvert, curves, soilLayer.Soil, soilLayer.IsAquifer, soilLayer.WaterpressureInterpolationModel); } /// /// Creates a 2D soil layer with points at based on its input arguments. /// /// The to create the 2D soil layer for. /// The collection of that defines the surface line. /// A collection of based on the input arguments. private static IEnumerable CreateSoilLayerWithSurfaceLineIntersection(SoilLayer1D soilLayer, IEnumerable surfaceLinePoints) { // Surface cannot be determined if there are not at least 2 coordinates var pointsArray = surfaceLinePoints.ToArray(); if (pointsArray.Length < 2) { return Enumerable.Empty(); } var enclosedAreas = GetEnclosedAreas(pointsArray, soilLayer.TopLevel, soilLayer.BottomLevel); return enclosedAreas.Select(a => CreateLayerData(a, soilLayer)).ToArray(); } /// /// Determines the enclosed areas that are formed by the intersections between the surface line, the top level and the bottom level. /// /// The collection of that define the surface line. /// The top level. /// The bottom level. /// A collection of . private static IEnumerable GetEnclosedAreas(IReadOnlyList surfaceLinePoints, double topLevel, double bottomLevel) { var currentArea = new EnclosedArea(); var enclosedAreas = new List(); double previousZCoordinate = surfaceLinePoints[0].Z; int j = 1; for (int i = 0; i < surfaceLinePoints.Count; i++) { Point2D currentPoint = surfaceLinePoints[i]; double xCoordinate = currentPoint.X; double zCoordinateSurfaceLine = currentPoint.Z; double nextSurfaceLineZCoordinate = surfaceLinePoints[j].Z; double currentZCoordinate = zCoordinateSurfaceLine; if (zCoordinateSurfaceLine > topLevel) { currentZCoordinate = topLevel; currentArea.AddCoordinate(new Point2D(xCoordinate, topLevel)); } else if (Math.Abs(zCoordinateSurfaceLine - bottomLevel) < GeometryConstants.Accuracy // Surface line is at layer bottom level || (zCoordinateSurfaceLine >= bottomLevel && zCoordinateSurfaceLine <= topLevel)) // Surface line is between the layer top and bottom level { currentArea.AddCoordinate(new Point2D(xCoordinate, zCoordinateSurfaceLine)); } // Determine if the current surface line point is located as a point on the horizontal line on the layer bottom level. // If yes, then a new area should be added if (Math.Abs(zCoordinateSurfaceLine - bottomLevel) < GeometryConstants.Accuracy && Math.Abs(nextSurfaceLineZCoordinate - bottomLevel) < GeometryConstants.Accuracy) { enclosedAreas.Add(currentArea); currentArea = new EnclosedArea(); } else if (previousZCoordinate > bottomLevel // Area should also be added when the area already started above or at the soil layer bottom && zCoordinateSurfaceLine < previousZCoordinate // The surface line has a decreasing trend w.r.t. the previous z coordinate && zCoordinateSurfaceLine <= bottomLevel) // The surface line crossed the bottom of the layer { enclosedAreas.Add(currentArea); currentArea = new EnclosedArea(); // If the next z coordinate is increasing, it means that the surface line is intersecting // the bottom level with an inflection point. In this situation, the current coordinate should be // added to the new area if (nextSurfaceLineZCoordinate > currentZCoordinate) { currentArea.AddCoordinate(new Point2D(xCoordinate, currentZCoordinate)); } } previousZCoordinate = currentZCoordinate; // End of array, take the last Z coordinate as the next coordinate j = i == surfaceLinePoints.Count - 2 ? j : j + 1; } // The last area needs to be added manually enclosedAreas.Add(currentArea); // Filter the areas that contain more than 1 coordinate return enclosedAreas.Where(area => area.Coordinates.Count() > 1); } private static LayerData CreateLayerData(EnclosedArea area, SoilLayer1D soilLayer) { double layerBottomLevel = soilLayer.BottomLevel; var pointsToConvert = new List(area.Coordinates); // Determine the additional points need to be added to close the area // The area ends at the top right point. Go clockwise to close the area by adding // points corresponding to the layer bottom level. var pointsToTraverse = area.Coordinates.Reverse(); foreach (Point2D point in pointsToTraverse) { var bottomPoint = new Point2D(point.X, layerBottomLevel); if (!point.LocationEquals(bottomPoint)) { pointsToConvert.Add(bottomPoint); } } var curves = GenerateClosedLoopCurves(pointsToConvert); return new LayerData(pointsToConvert, curves, soilLayer.Soil, soilLayer.IsAquifer, soilLayer.WaterpressureInterpolationModel); } /// /// Generates curves based on a collection of points. /// /// The collection points to generate a curve collection for. /// A collection of . private static IEnumerable GenerateCurves(IReadOnlyList pointsToConvert) { Point2D startPoint = pointsToConvert[0]; var curves = new List(); for (int i = 1; i < pointsToConvert.Count; i++) { Point2D endPoint = pointsToConvert[i]; curves.Add(new GeometryCurve(startPoint, endPoint)); startPoint = endPoint; } return curves; } /// /// Generates curves that define a clockwise closed loop based on a collection of points. /// /// The collection points to generate a curve collection for. /// A collection of . private static IEnumerable GenerateClosedLoopCurves(IReadOnlyList pointsToConvert) { var curves = GenerateCurves(pointsToConvert).ToList(); // Close the geometry by connecting the first and last points with each other // This closes the outer loop in a clockwise definition. curves.Add(new GeometryCurve(pointsToConvert.Last(), pointsToConvert.First())); return curves; } /// /// Class to hold the geometry data of a layer. /// private class LayerData { /// /// Creates a new instance of . /// /// The collection of . /// The collection of . /// The belonging to this layer. /// Indicator whether the layer is an aquifer or not. /// The of this layer. public LayerData(IEnumerable points, IEnumerable curves, Soil soil, bool isAquifer, WaterpressureInterpolationModel waterpressureInterpolationModel) { Points = points; Curves = curves; Soil = soil; IsAquifer = isAquifer; WaterpressureInterpolationModel = waterpressureInterpolationModel; } /// /// Gets the collection of that define this layer. /// public IEnumerable Points { get; } /// /// Gets the collection of that define this layer. /// public IEnumerable Curves { get; } /// /// Gets the soil of this layer. /// public Soil Soil { get; } /// /// Gets the indicator whether this layer is an aquifer. /// public bool IsAquifer { get; } /// /// Gets the belonging to this layer. /// public WaterpressureInterpolationModel WaterpressureInterpolationModel { get; } /// /// Function to determine whether the z coordinate lies between the top and /// bottom coordinate of this layer. /// /// The z coordinate to determine the whether it is located inside this layer. /// true if z lies between the top and bottom coordinate of this layer, false otherwise public bool IsZLocatedInLayer(double z) { double maxZCoordinate = Points.Select(p => p.Z).Max(); double minZCoordinate = Points.Select(p => p.Z).Min(); return z >= minZCoordinate && z <= maxZCoordinate; } } /// /// Class to hold the information about the area between a surface line and a soil layer. /// private class EnclosedArea { private readonly List coordinates; /// /// Creates a new instance of . /// public EnclosedArea() { coordinates = new List(); } /// /// Gets the coordinates of the enclosed area. /// public IEnumerable Coordinates { get{ return coordinates; }} /// /// Adds a coordinate to the area. /// /// The coordinate to add. /// Thrown when /// is null. public void AddCoordinate(Point2D coordinate) { if (coordinate == null) { throw new ArgumentNullException(nameof(coordinate)); } coordinates.Add(coordinate); } } } }