// 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.Linq;
using Deltares.DamEngine.Data.Standard;
namespace Deltares.DamEngine.Data.Geometry;
///
/// Classifications of an objected with regards to a geometric line in the XZ-plane.
///
public enum RelativeXzPosition
{
///
/// Indicates that the object is considered 'on' the geometric line.
///
OnGeometricLine,
///
/// Indicates that the object is considered 'above' the geometric line (object Z
/// coordinate higher than that of the line).
///
AboveGeometricLine,
///
/// Indicates that the object is considered 'below' the geometric line (object Z
/// coordinates lower than that of the line).
///
BelowGeometricLine,
///
/// Indicates that the object is considered 'outside the scope' of the geometric line.
///
BeyondGeometricLine
}
///
/// Type of extrapolation
///
public enum ExtraPolationMode
{
///
/// No extrapolation should be used.
///
Beyond,
///
/// Used constant (0th order) extrapolation at the extremes.
///
Horizontal
}
///
/// Collection of points (X,Z) in world coordinates used for describing Surface lines
///
public class GeometryPointString : GeometryObject
{
///
/// Matching distance where a point within this range is considered on the same point.
///
private const double epsilon = GeometryConstants.Accuracy;
///
/// The calculate points as protected field (to be able to prevent recursive calls to CalcPoints)
///
protected List calcPoints = new List();
///
/// Gets the at the specified index.
///
///
/// The .
///
/// The index.
/// When less
/// than zero or is greater or equals to .
public Point2D this[int index]
{
get
{
return calcPoints[index];
}
}
///
/// The calculate points (to be used in calculation instead of Points for better performance)
///
public virtual List CalcPoints
{
get
{
return calcPoints;
}
}
///
/// Gets the count of the points.
///
///
/// The count.
///
public int Count
{
get
{
return calcPoints.Count;
}
}
///
/// Clones this object except the points
///
///
public virtual GeometryPointString Clone()
{
var clone = new GeometryPointString();
this.CloneProperties(clone); // excludes the points !
clone.CalcPoints.Clear();
foreach (Point2D point in CalcPoints)
{
clone.CalcPoints.Add(new Point2D(point.X, point.Z));
}
return clone;
}
///
/// Round all points to the nearest given accuracy
///
public void RoundPointsCoordinates(double accuracy)
{
foreach (Point2D point in CalcPoints)
{
point.X = RoundValue(point.X, accuracy);
point.Z = RoundValue(point.Z, accuracy);
}
}
///
/// Gets the minimum z.
/// Note: CalcPoints must be used instead of calcPoints as otherwise unclear behaviour of Linq spoils the result
/// Change back to calcPoints and Benchmark4_01l in the WaternetCreatorTests will fail!
///
///
public double GetMinZ()
{
return CalcPoints.Any()
? CalcPoints.Min(p => p.Z)
: double.NaN;
}
///
/// Gets the maximum z.
/// Note: CalcPoints must be used instead of calcPoints as otherwise unclear behaviour of Linq spoils the result
/// Change back to calcPoints and Benchmark4_01l in the WaternetCreatorTests will fail!
///
///
public double GetMaxZ()
{
return CalcPoints.Any()
? CalcPoints.Max(p => p.Z)
: double.NaN;
}
///
/// The minimal X value among .
/// Note: CalcPoints must be used instead of calcPoints as otherwise unclear behaviour of Linq spoils the result
/// Change back to calcPoints and Benchmark4_01l in the WaternetCreatorTests will fail!
///
/// The minimal X value or in case there are no points.
public double GetMinX()
{
return CalcPoints.Any(p => !double.IsNaN(p.X))
? CalcPoints.Where(p => !double.IsNaN(p.X)).Min(p => p.X)
: double.NaN;
}
///
/// Gets the maximum x.
/// Note: CalcPoints must be used instead of calcPoints as otherwise unclear behaviour of Linq spoils the result
/// Change back to calcPoints and Benchmark4_01l in the WaternetCreatorTests will fail!
///
///
public double GetMaxX()
{
return CalcPoints.Any(p => !double.IsNaN(p.X))
? CalcPoints.Where(p => !double.IsNaN(p.X)).Max(p => p.X)
: double.NaN;
}
///
/// Finds all vertical XZ intersections with this geometric point string, returning
/// the highest value among the intersections.
///
/// X coordinate
///
/// The z value determined by or
/// when is empty.
///
///
/// Uses constant extrapolation and linear interpolation.
///
public double GetZAtUnsortedX(double x)
{
if (calcPoints.Count > 0)
{
var verticalLineAtX = new Line(new Point2D
{
X = x,
Z = GetMaxZ()
},
new Point2D
{
X = x,
Z = GetMinZ()
});
if (Math.Abs(verticalLineAtX.BeginPoint.Z - verticalLineAtX.EndPoint.Z) < GeometryConstants.Accuracy)
{
verticalLineAtX.BeginPoint.Z += 1.0;
verticalLineAtX.EndPoint.Z -= 1.0;
}
IList intersectionPoints = IntersectionPointsXzWithLineXz(verticalLineAtX);
if (intersectionPoints.Count != 0)
{
return intersectionPoints.Max(gp => gp.Z);
}
// Remain consistent with GetZAtX, do constant extrapolation:
double zAtX;
if (DoConstantExtrapolationXz(x, out zAtX))
{
return zAtX;
}
}
return double.NaN;
}
///
/// Retrieves the Z value for the given x.
///
/// X coordinate
///
/// The z value determined by or
/// when is empty.
///
///
/// Uses constant extrapolation and linear interpolation.
///
public double GetZatX(double x)
{
if (calcPoints.Any())
{
double zAtX;
if (DoConstantExtrapolationXz(x, out zAtX))
{
return zAtX;
}
}
for (var i = 0; i < calcPoints.Count - 1; i++)
{
Point2D current = calcPoints[i];
Point2D next = calcPoints[i + 1];
double leftOffset = x - current.X;
double rightOffset = next.X - x;
if (Math.Abs(leftOffset) < epsilon)
{
return current.Z;
}
if (Math.Abs(rightOffset) < epsilon)
{
return next.Z;
}
if (leftOffset >= 0 && rightOffset >= 0)
{
double fraction = leftOffset / (leftOffset + rightOffset);
return (1.0 - fraction) * current.Z + fraction * next.Z;
}
}
return Double.NaN;
}
///
/// Gets all z-values at x for a line.
///
/// The x.
///
public List GetAllZatXForLine(double x)
{
return GetAllZatX(x, false);
}
///
/// Finds the first intersection of the line with a given horizontal line.
///
/// The height level of the horizontal line.
/// Optional: Discard intersections whose X value is less then
/// or equal to this value.
/// The first intersection point matching the criteria.
///
public double GetXatZ(double z, double xmin = Double.MinValue)
{
return GetXatZStartingAt(0, z, xmin);
}
///
/// Finds the first intersection of the line with a given horizontal line.
///
/// Start considering instances from
/// starting from this index.
/// The height level of the horizontal line.
/// Optional: Discard intersections whose X value is less than or equal to this value.
/// The first intersection point matching the criteria.
///
public double GetXatZStartingAt(int start, double z, double xMin = double.MinValue)
{
for (int i = start; i < calcPoints.Count - 1; i++)
{
Point2D current = calcPoints[i];
Point2D next = calcPoints[i + 1];
if (IsSegmentNotCrossingZ(z, current, next)) // Performance micro-optimization
{
continue;
}
double x = GetXIntersectingZ(z, current, next);
if (x > xMin)
{
return x;
}
}
return double.NaN;
}
///
/// Gets the points at x.
///
/// The x.
///
public IEnumerable DeterminePointsAtX(double x)
{
return from Point2D point in calcPoints
where point.X.AlmostEquals(x, GeometryPoint.Precision)
select point;
}
///
/// Gets the Point2D at the specified coordinates.
///
/// The x.
/// The z.
///
public Point2D DeterminePointAt(double x, double z)
{
return calcPoints.FirstOrDefault(
point => point.X.AlmostEquals(x, GeometryPoint.Precision) &&
point.Z.AlmostEquals(z, GeometryPoint.Precision));
}
///
/// Returns ALL intersection points that are found for a line costructed at level z,
/// including the double ones as the number of points can be relevant!
///
/// the level at which the line for intersections is constructed.
///
public IList IntersectionsXAtZ(double z)
{
var intersectionsX = new List();
var lineAtZ = new Line
{
BeginPoint = new Point2D(GetMinX(), z),
EndPoint = new Point2D(GetMaxX(), z)
};
intersectionsX.AddRange(IntersectionPointsXzWithLineXzWithAllPoints(lineAtZ).Select(gp => gp.X));
return intersectionsX;
}
///
/// Returns ALL intersection points that are found, including the double ones as the number of points can be relevant!
///
///
///
public IList IntersectionPointsXzWithLineXzWithAllPoints(Line line)
{
return IntersectionPointsWithLineCore(line, true);
}
///
/// Returns the UNIQUE intersectionpoints (so can not be used where number of interscections is relevant)
///
///
///
public IList IntersectionPointsXzWithLineXz(Line line)
{
return IntersectionPointsWithLineCore(line, false);
}
///
/// Finds all intersections in the XZ-plane the given .
///
/// The geometry point string.
///
///
public List IntersectionXzPointsWithGeometryString(GeometryPointString externalSurface)
{
return IntersectionXzPointsWithGeometryPointList(externalSurface.CalcPoints);
}
///
/// Sorts the points by x ascending (only to be used for Surface lines).
///
public virtual void SortPointsByXAscending()
{
calcPoints.Sort();
}
///
/// Removes all double points at a location, if consecutive
///
private void CondensePoints()
{
for (int i = calcPoints.Count - 1; i > 0; i--)
{
if (calcPoints[i].LocationEquals(calcPoints[i - 1]))
{
calcPoints.RemoveAt(i);
}
}
}
///
/// Removes all points which don't influence the shape of the line
///
public void RemoveUnnecessaryPoints()
{
const double slopeTolerance = 1E-10;
CondensePoints();
for (int i = calcPoints.Count - 2; i > 0; i--)
{
// if the slope of the line before the point is equal to the slope after the point, the point can be removed
double slopeBefore = (calcPoints[i].Z - calcPoints[i - 1].Z) / (calcPoints[i].X - calcPoints[i - 1].X);
double slopeAfter = (calcPoints[i + 1].Z - calcPoints[i].Z) / (calcPoints[i + 1].X - calcPoints[i].X);
if (Routines2D.AreEqual(slopeBefore, slopeAfter, slopeTolerance))
{
calcPoints.RemoveAt(i);
}
}
}
///
/// Finds all intersections in the XZ-plane the given list.
///
/// The list.
///
///
private List IntersectionXzPointsWithGeometryPointList(IList list)
{
return IntersectWithPointsListCore(list, false);
}
///
/// Determines the relative position of a point to the geometric line in the XZ-plane.
///
///
///
///
public RelativeXzPosition GetPosition(GeometryLoop geometryLoop, ExtraPolationMode extraPolationMode = ExtraPolationMode.Beyond)
{
foreach (Point2D point in geometryLoop.CalcPoints.Concat(DetermineLoopSegmentMiddlePoints(geometryLoop)))
{
RelativeXzPosition position = PositionXzOfPointRelatedToExtrapolatedLine(point, extraPolationMode);
if (position == RelativeXzPosition.BeyondGeometricLine)
{
position = RelativeXzPosition.BelowGeometricLine;
}
if (position != RelativeXzPosition.OnGeometricLine)
{
return position;
}
}
return RelativeXzPosition.OnGeometricLine;
}
///
/// Gets the surrounding rectangle around the geometry point string
///
///
public override GeometryBounds GetGeometryBounds()
{
GeometryBounds bounds = CalcPoints[0].GetGeometryBounds();
for (var i = 1; i < CalcPoints.Count; i++)
{
Point2D point = CalcPoints[i];
bounds.Left = Math.Min(bounds.Left, point.X);
bounds.Right = Math.Max(bounds.Right, point.X);
bounds.Top = Math.Max(bounds.Top, point.Z);
bounds.Bottom = Math.Min(bounds.Bottom, point.Z);
}
return bounds;
}
private IList IntersectionPointsXzClosedStringWithLineXz(Line line)
{
IList intersectionPointsWithLine = IntersectionPointsXzWithLineXz(line);
// close the poly line
if (calcPoints.Count > 0)
{
DoIntersectAndAddToCollection(line, calcPoints[calcPoints.Count - 1], calcPoints[0],
intersectionPointsWithLine, false);
}
return intersectionPointsWithLine;
}
private RelativeXzPosition PositionXzOfPointRelatedToExtrapolatedLine(Point2D point,
ExtraPolationMode extraPolationMode = ExtraPolationMode.Beyond)
{
return IsPointConsideredBeyondLine(point, extraPolationMode) ? RelativeXzPosition.BeyondGeometricLine : DeterminePositionWithRespectToExtrapolatedLine(point, extraPolationMode);
}
private bool IsPointConsideredBeyondLine(Point2D point, ExtraPolationMode extraPolationMode)
{
if (CalcPoints.Count == 0)
{
return true;
}
return IsPointOutsideRangeX(point) && extraPolationMode == ExtraPolationMode.Beyond;
}
private bool IsPointOutsideRangeX(Point2D point)
{
return point.X < CalcPoints[0].X || point.X > CalcPoints[checked(CalcPoints.Count - 1)].X;
}
private RelativeXzPosition DeterminePositionWithRespectToExtrapolatedLine(Point2D point,
ExtraPolationMode extraPolationMode)
{
double xToEvaluate = DetermineXToEvaluate(point, extraPolationMode);
double zAtX = GetZatX(xToEvaluate);
if (Math.Abs(point.Z - zAtX) < 0.001)
{
return RelativeXzPosition.OnGeometricLine;
}
return point.Z <= zAtX ? RelativeXzPosition.BelowGeometricLine : RelativeXzPosition.AboveGeometricLine;
}
private double DetermineXToEvaluate(Point2D point, ExtraPolationMode extraPolationMode)
{
double x = point.X;
if (extraPolationMode == ExtraPolationMode.Horizontal)
{
if (x < CalcPoints[0].X)
{
x = CalcPoints[0].X;
}
else if (x > CalcPoints[checked(CalcPoints.Count - 1)].X)
{
x = CalcPoints[checked(CalcPoints.Count - 1)].X;
}
}
return x;
}
private static IEnumerable DetermineLoopSegmentMiddlePoints(GeometryPointString geometryLoop)
{
var loopSegmentMiddlePoints = new List();
var index = 0;
while (index < checked(geometryLoop.CalcPoints.Count - 1))
{
AddLoopSegmentMiddlePoint(geometryLoop.CalcPoints[index], geometryLoop.CalcPoints[checked(index + 1)], loopSegmentMiddlePoints);
checked
{
++index;
}
}
if (geometryLoop.CalcPoints.Count > 2)
{
int last = geometryLoop.CalcPoints.Count - 1;
AddLoopSegmentMiddlePoint(geometryLoop.CalcPoints[0], geometryLoop.CalcPoints[last], loopSegmentMiddlePoints);
}
return loopSegmentMiddlePoints;
}
private static void AddLoopSegmentMiddlePoint(Point2D point1, Point2D point2, IList loopSegmentMiddlePoints)
{
if (Math.Abs(point1.Z - point2.Z) < 0.001)
{
loopSegmentMiddlePoints.Insert(0, new Point2D((point1.X + point2.X) / 2.0, point1.Z));
}
else
{
loopSegmentMiddlePoints.Add(new Point2D((point1.X + point2.X) / 2.0, (point1.Z + point2.Z) / 2.0));
}
}
///
/// Checks if constant extrapolation can be applied, and if so set the Z value.
///
/// The evaluated X coordinate.
/// Output param: Extrapolated Z value, or
/// when no extrapolation is possible.
/// True if extrapolation possible; false otherwise.
private bool DoConstantExtrapolationXz(double x, out double z)
{
if (calcPoints.Count > 0)
{
Point2D first = calcPoints[0];
if (x < first.X || Math.Abs(x - first.X) < epsilon)
{
z = first.Z;
return true;
}
Point2D last = calcPoints[calcPoints.Count - 1];
if (x > last.X)
{
z = last.Z;
return true;
}
}
z = double.NaN;
return false;
}
///
/// Can be used for a Line or for a Surface where a surface is supposed to closed.
/// In case of a waternet line it is possible
/// to have more z values at a give x coor
/// Furthermore a z is not needed at al x values
///
///
///
///
private List GetAllZatX(double x, bool asSurface)
{
var result = new List();
if (calcPoints.Count == 1 && Math.Abs(calcPoints[0].X - x) < epsilon)
{
result.Add(calcPoints[0].Z);
return result;
}
int pointsCount = calcPoints.Count - 1;
if (asSurface)
{
pointsCount = calcPoints.Count;
}
for (var i = 0; i < pointsCount; i++)
{
Point2D current;
Point2D next;
if (i == calcPoints.Count - 1)
{
current = calcPoints[i];
next = calcPoints[0];
}
else
{
current = calcPoints[i];
next = calcPoints[i + 1];
}
double leftOffset = x - current.X;
double rightOffset = next.X - x;
var matchedWithAPoint = false;
if (Math.Abs(leftOffset) < epsilon)
{
result.Add(current.Z);
matchedWithAPoint = true;
}
if (Math.Abs(rightOffset) < epsilon)
{
result.Add(next.Z);
matchedWithAPoint = true;
}
if (!matchedWithAPoint)
{
if (leftOffset > 0 && rightOffset > 0)
{
double fraction = leftOffset / (leftOffset + rightOffset);
result.Add((1.0 - fraction) * current.Z + fraction * next.Z);
}
// if both ofsets are negative the waterline goes back
if ((leftOffset < 0) && (rightOffset < 0))
{
double fraction = rightOffset / (rightOffset + leftOffset);
result.Add((1.0 - fraction) * next.Z + fraction * current.Z);
}
}
}
return result.Distinct().ToList();
}
private static bool IsSegmentNotCrossingZ(double z, Point2D current, Point2D next)
{
if (double.IsNaN(z))
{
return true;
}
double leftOffset = Math.Abs(current.Z - z);
double rightOffset = Math.Abs(next.Z - z);
int currentSign = leftOffset < epsilon ? 0 : Math.Sign(current.Z - z);
int nextSign = rightOffset < epsilon ? 0 : Math.Sign(next.Z - z);
return currentSign == nextSign && currentSign != 0;
}
private static double GetXIntersectingZ(double z, Point2D current, Point2D next)
{
double leftOffset = Math.Abs(current.Z - z);
if (leftOffset < epsilon)
{
return current.X;
}
double rightOffset = Math.Abs(next.Z - z);
if (rightOffset < epsilon)
{
return next.X;
}
double fraction = leftOffset / (leftOffset + rightOffset);
return GeneralMathRoutines.LinearInterpolate(current.X, next.X, fraction);
}
private IList IntersectionPointsWithLineCore(Line line, bool allowDuplicates)
{
var intersectionPointsWithLine = new List();
for (var pointIndex = 0; pointIndex < calcPoints.Count - 1; pointIndex++)
{
DoIntersectAndAddToCollection(line, calcPoints[pointIndex], calcPoints[pointIndex + 1],
intersectionPointsWithLine, allowDuplicates);
}
return intersectionPointsWithLine;
}
private static void DoIntersectAndAddToCollection(Line line, Point2D begin, Point2D end, ICollection intersectionPointsWithLine, bool allowDuplicates)
{
var lineInPoly = new Line
{
BeginPoint = begin,
EndPoint = end
};
Point2D intersectionPoint = lineInPoly.GetIntersectPointXz(line);
if (intersectionPoint != null && (allowDuplicates || NoPointSameXzLocation(intersectionPointsWithLine, intersectionPoint)))
{
intersectionPointsWithLine.Add(intersectionPoint);
}
}
private static bool NoPointSameXzLocation(IEnumerable collection, Point2D point)
{
return !collection.Any(
p => Math.Abs(p.X - point.X) < GeometryConstants.Accuracy &&
Math.Abs(p.Z - point.Z) < GeometryConstants.Accuracy);
}
private List IntersectWithPointsListCore(IList list, bool closePointString)
{
var result = new List();
var line = new Line();
for (var externalPointIndex = 0; externalPointIndex < list.Count - 1; externalPointIndex++)
{
line.BeginPoint = list[externalPointIndex];
line.EndPoint = list[externalPointIndex + 1];
result.AddRange(!closePointString
? IntersectionPointsXzWithLineXzWithAllPoints(line)
: IntersectionPointsXzClosedStringWithLineXz(line));
}
return result;
}
}