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