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