// Copyright (C) Stichting Deltares 2023. 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; private readonly List points = new List(); /// /// The calculate points as protected field (to be able to prevent recursive calls to CalcPoints) /// protected readonly List calcPoints = new List(); private bool isFrozen; private bool hasNaNx; private double frozenMaxZ = double.NaN; // sortedPoints must never be used outside this class. Either the GPS concerned must have sorted points but then they already are // (eg. surfaceline, headline) or they may be unsorted in which case using the sorted list in other classes leads to errors (eg. // geometrysurfaces, waternetlines) private List sortedPoints; /// /// The calculate points (to be used in calcualtion instead of Points for better performance) /// public virtual List CalcPoints { get { return calcPoints; } } /// /// Freezes this instance. /// public void Freeze() { if (!isFrozen) { sortedPoints = new List(calcPoints.Count); foreach (Point2D point in calcPoints) { sortedPoints.Add(point); hasNaNx = hasNaNx || double.IsNaN(point.X); frozenMaxZ = Math.Max(frozenMaxZ, point.Z); } sortedPoints.Sort(); } isFrozen = true; } /// /// List of points that describe the physical surface line or surface. /// /// /// The points. /// public virtual IList Points { get { return points; } } /// /// 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]; } } /// /// 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); // exludes the points ! clone.Points.Clear(); foreach (var point in Points) { clone.Points.Add(new GeometryPoint(point)); } clone.SyncCalcPoints(); return clone; } /// /// 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() { if (isFrozen) return frozenMaxZ; 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() { if (isFrozen && !hasNaNx) return sortedPoints[0].X; 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() { if (isFrozen && !hasNaNx) return sortedPoints[sortedPoints.Count - 1].X; 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; } var 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 virtual double GetZatX(double x) { if (calcPoints.Any()) { double zAtX; if (DoConstantExtrapolationXz(x, out zAtX)) { return zAtX; } } for (int i = 0; i < calcPoints.Count - 1; i++) { var current = calcPoints[i]; var next = calcPoints[i + 1]; var leftOffset = x - current.X; var 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) { var fraction = leftOffset / (leftOffset + rightOffset); return (1.0 - fraction) * current.Z + fraction * next.Z; } } return Double.NaN; } /// /// Gets the z at x starting from index i in the point list. /// This is only meant as a fast(er) version for GetZAtX for lists that are sorted by X, so use with care. /// /// The x. /// The i. /// public virtual double GetZatX(double x, ref int i) { for (; i < calcPoints.Count - 1; i++) { var current = calcPoints[i]; var leftOffset = x - current.X; var next = calcPoints[i + 1]; var rightOffset = next.X - x; if (leftOffset < epsilon) { return current.Z; } if (rightOffset >= epsilon) { var fraction = leftOffset / (leftOffset + rightOffset); return (1.0 - fraction) * current.Z + fraction * next.Z; } if (i + 1 == calcPoints.Count - 1) { return 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); } /// /// Gets all z values at x for a surface. /// /// The x. /// public List GetAllZatXForSurface(double x) { return GetAllZatX(x, true); } /// /// 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 then /// 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++) { var current = calcPoints[i]; var next = calcPoints[i + 1]; if (IsSegmentNotCrossingZ(z, current, next)) // Performance micro-optimization { continue; } var x = GetXIntersectingZ(z, current, next); if (x > xmin) { return x; } } return Double.NaN; } /// /// Gets the points at x. /// /// The x. /// public IEnumerable GetPointsAtX(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 GetPointAt(double x, double z) { return calcPoints.FirstOrDefault( point => point.X.AlmostEquals(x, GeometryPoint.Precision) && point.Z.AlmostEquals(z, GeometryPoint.Precision)); } /// /// Gets the GeometryPoint at the specified coordinates. /// /// The x. /// The z. /// public GeometryPoint GetGeometryPointAt(double x, double z) { return Points.FirstOrDefault( point => point.X.AlmostEquals(x, GeometryPoint.Precision) && point.Z.AlmostEquals(z, GeometryPoint.Precision)); } //#Bka: this should never be 1 method! Split it into 2 methods // It is used for sliplanes only and therefor uses Points instead of calcPoints /// /// Adds the or remove list object. /// /// a point. /// if set to true [a to add]. /// a index. public virtual void AddOrRemoveListObject(object aPoint, bool aToAdd, int aIndex) { var point = aPoint as GeometryPoint; if (point == null) { return; } if (aToAdd) { Points.Insert(aIndex, point); //#bka what happens if index already exists? what if point exists? What if both??? } else { if (Points.Contains(point)) { Points.Remove(point); } } } /// /// 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); } 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; } /// /// Find intersection (xz-plane) from this surface with another surface /// or phratic line /// /// /// /// Considers all in to /// such that they describe a closed loop. /// public IList ClosedGeometryIntersectionXzPointsWithGeometryPointList(IList list) { return IntersectWithPointsListCore(list, true); } /// /// Finds all intersections in the XZ-plane the given list. /// /// The list. /// /// /// private List IntersectionXzPointsWithGeometryPointList(IList list) { return IntersectWithPointsListCore(list, 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(); points.Sort(); } /// /// Removes the non ascending points on surface line. /// public void RemoveNonAscendingPointsOnSurfaceLine() { for (int i = points.Count - 1; i > 0; i--) { if (Math.Abs(points[i].X - points[i - 1].X) < GeometryConstants.Accuracy) { if (Math.Abs(points[i].Z - points[i - 1].Z) > GeometryConstants.Accuracy) { points.Remove(points[i]); } } } } /// /// Gets a part of the geometry point string defined by a begin and end point /// /// /// /// public GeometryPointString GetPart(double begin, double end) { var part = new GeometryPointString(); bool filling = false; bool filled = false; for (int i = 0; i < calcPoints.Count; i++) { if (!filling && !filled) { filling = calcPoints[i].X >= begin - epsilon; } else { filled = calcPoints[i].X >= end - epsilon; if (filled) { filling = false; } } if (filling) { part.calcPoints.Add(calcPoints[i]); } } var beginPoint = new Point2D(begin, GetZatX(begin)); var endPoint = new Point2D(end, GetZatX(end)); if (part.calcPoints.Count == 0) { part.calcPoints.Add(beginPoint); part.calcPoints.Add(endPoint); } if (!part.calcPoints[0].LocationEquals(beginPoint)) { part.calcPoints.Insert(0, beginPoint); } if (!part.calcPoints[part.calcPoints.Count - 1].LocationEquals(endPoint)) { part.calcPoints.Add(endPoint); } SyncPoints(); return part; } /// /// Removes all double points at a location, if consecutive /// public 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 > 1; 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); } } } /// /// Gets the surrounding rectangle around the geometry point string /// /// public override GeometryBounds GetGeometryBounds() { if (!Points.Any()) { // Sync with calcPoints SyncPoints(); // if still no points, then return null if (!Points.Any()) return null; } GeometryBounds bounds = Points[0].GetGeometryBounds(); for (var i = 1; i < Points.Count; i++) { GeometryPoint point = Points[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; } /// /// 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) { var first = calcPoints[0]; if (x < first.X || Math.Abs(x - first.X) < epsilon) { z = first.Z; return true; } var 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); } var pointsCount = calcPoints.Count - 1; if (asSurface) { pointsCount = calcPoints.Count; } for (int 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]; } var leftOffset = x - current.X; var 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) { var 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)) { var 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; } var leftOffset = Math.Abs(current.Z - z); var 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) { var leftOffset = Math.Abs(current.Z - z); if (leftOffset < epsilon) { return current.X; } var rightOffset = Math.Abs(next.Z - z); if (rightOffset < epsilon) { return next.X; } var fraction = leftOffset / (leftOffset + rightOffset); return GeneralMathRoutines.LinearInterpolate(current.X, next.X, fraction); } private IList IntersectionPointsWithLineCore(Line line, bool allowDuplicates) { var intersectionPointsWithLine = new List(); for (int 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 (int 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; } /// /// Synchronizes the calculation points. /// public void SyncCalcPoints() { calcPoints.Clear(); foreach (var geometryPoint in Points) { var p2D = new Point2D { X = geometryPoint.X, Z = geometryPoint.Z }; calcPoints.Add(p2D); } } /// /// Synchronizes the points. /// public void SyncPoints() { points.Clear(); foreach (var p2D in calcPoints) { var geometryPoint = new GeometryPoint { X = p2D.X, Z = p2D.Z }; points.Add(geometryPoint); } } } }