// Copyright (C) Stichting Deltares 2025. 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; namespace Deltares.DamEngine.Data.Geometry; /// /// Class for geometry surfaces. /// /// [Serializable] public class GeometrySurface : GeometryObject { private GeometryLoop outerLoop = new(); /// /// Empty constructor /// public GeometrySurface() {} /// /// Initializes a new instance of the class. /// /// The loop. public GeometrySurface(GeometryLoop loop) { outerLoop = loop; } /// /// The circumference of the surface. /// public GeometryLoop OuterLoop { get { return outerLoop; } set { outerLoop = value; } } /// /// All internal loops encompassed by . /// public List InnerLoops { get; } = new List(); /// /// Placeholder for the previous outer loop. /// public GeometryLoop PreviousOuterLoop { get; private set; } = new GeometryLoop(); public List PreviousInnerLoops { get; } = new List(); /// /// Determines if there are any inner loops in this surface /// /// true when at least 1 inner loop is available else false public bool HasInnerLoops() { return InnerLoops.Any(); } /// /// Sets the outer loop to loop whilst storing the "old" value as previousOuterLoop. /// /// public void SetOuterLoop(GeometryLoop loop) { if (loop == OuterLoop) { return; } PreviousOuterLoop = OuterLoop; OuterLoop = loop; } /// /// Add to if it isn't already /// in that collection. /// public void AddInnerLoop(GeometryLoop aLoop) { if (!InnerLoops.Contains(aLoop) && !HasIdenticalInnerLoop(aLoop)) { InnerLoops.Add(aLoop); } } /// /// Remove all inner loops from the surface whilst updating the previous inner loops list. /// public void RemoveAllInnerLoops() { PreviousInnerLoops.Clear(); if (InnerLoops.Count <= 0) { return; } PreviousInnerLoops.AddRange(InnerLoops); InnerLoops.Clear(); } /// /// determine top of geometry surface outerloop as GeometryPointString /// /// public GeometryPointString DetermineTopGeometrySurface() { return DetermineTopCurves(); } /// /// determine bottom of geometry surface outerloop as GeometryPointString /// /// public GeometryPointString DetermineBottomGeometrySurface() { return DetermineBottomCurves(); } /// /// Creates a clone of the geometry surface /// /// The cloned GeometrySurface public GeometrySurface Clone() { // Only clone the name, the outer loop and the inner loops are not to be cloned here! var clonedGeometrySurface = new GeometrySurface { Name = Name }; return clonedGeometrySurface; } /// /// Gets the geometry bounds. /// /// public override GeometryBounds GetGeometryBounds() { return OuterLoop.GetGeometryBounds(); } /// /// Determines a point within the outer loop of the new surface and that is not in an inner loop of that surface /// so it can be used to determine the old surface. /// /// /// The point or null when no valid point is found. public Point2D DetermineValidTestPointBasedOnNewSurface() { const double initialOffset = 0.002; const double stopCriteriumOffset = 1e-8; double offset = initialOffset; Point2D point = null; var geometryPointString = DetermineTopGeometrySurface(); geometryPointString.SortPointsByXAscending(); while (point == null && offset > stopCriteriumOffset) { // Just keep looking by halving the offset until found. point = FindValidTestPoint(geometryPointString, offset); offset /= 2.0; } return point; } private Point2D FindValidTestPoint(GeometryPointString geometryPointString, double offset) { var point = new Point2D(); for (int i = 0; i < geometryPointString.Count - 1; i++) { // look if point just below the top line is in the outer loop and if so, is not in a inner loop // If both are true, then a valid test point is found, else keep looking point.X = (geometryPointString[i].X + geometryPointString[i + 1].X) * 0.5; point.Z = (geometryPointString[i].Z + geometryPointString[i + 1].Z) * 0.5 - offset; if (Routines2D.CheckIfPointIsInPolygon(OuterLoop, point.X, point.Z) == PointInPolygon.InsidePolygon) { var isInInnerLoop = false; foreach (GeometryLoop innerLoop in InnerLoops) { if (Routines2D.CheckIfPointIsInPolygon(innerLoop, point.X, point.Z) == PointInPolygon.InsidePolygon) { isInInnerLoop = true; } } if (!isInInnerLoop) { break; } } if (i == geometryPointString.Count - 2) { // if no valid point is yet found, then the shape is very awkward or the surface is terribly small. // In that case, return null so the routine can try again with a smaller offset. point = null; } } return point; } private bool HasIdenticalInnerLoop(GeometryLoop loop) { return InnerLoops.Any(innerLoop => innerLoop.HasSameCurves(loop)); } /// /// Determine points at the top side of the geometry surface /// /// private GeometryPointString DetermineTopCurves() { IEnumerable minXPoints = OuterLoop.DeterminePointsAtX(OuterLoop.GetMinX()); IEnumerable maxXPoints = OuterLoop.DeterminePointsAtX(OuterLoop.GetMaxX()); //verticals at start are omitted Point2D startTopPoint = minXPoints.OrderByDescending(p => p.Z).First(); //verticals at end are omitted Point2D endTopPoint = maxXPoints.OrderByDescending(p => p.Z).First(); var topPoints = new GeometryPointString(); int currentIndex = OuterLoop.Points.IndexOf(startTopPoint); FillTopOrBottomPoints(topPoints, endTopPoint, currentIndex); return topPoints; } /// /// determine curves at the bottom side of the geometry surface /// /// private GeometryPointString DetermineBottomCurves() { //create copy, leave original IEnumerable minXPoints = OuterLoop.DeterminePointsAtX(OuterLoop.GetMinX()); IEnumerable maxXPoints = OuterLoop.DeterminePointsAtX(OuterLoop.GetMaxX()); //verticals at start are omitted Point2D startBottomPoint = minXPoints.OrderBy(p => p.Z).First(); //verticals at end are omitted Point2D endBottomPoint = maxXPoints.OrderBy(p => p.Z).First(); var bottomPoints = new GeometryPointString(); int currentIndex = OuterLoop.Points.IndexOf(endBottomPoint); FillTopOrBottomPoints(bottomPoints, startBottomPoint, currentIndex); // As the curves are sorted clockwise, the bottom curves are always orientated from right to left // while this result of this method must be from left to right so reverse the curves bottomPoints.Points.Reverse(); return bottomPoints; } private void FillTopOrBottomPoints(GeometryPointString linePoints, Point2D endPoint, int currentIndex) { while (!linePoints.Points.Contains(endPoint)) { linePoints.Points.Add(OuterLoop.Points[currentIndex++]); if (currentIndex >= OuterLoop.Points.Count) { currentIndex = 0; } } for (var i = 0; i < linePoints.Points.Count; i++) { linePoints.Points[i] = new Point2D(linePoints.Points[i].X, linePoints.Points[i].Z); } } }