// Copyright (C) Stichting Deltares 2017. All rights reserved. // // This file is part of Ringtoets. // // Ringtoets is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser 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 Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser 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.Collections.ObjectModel; using System.Linq; using Core.Common.Base.Properties; using MathNet.Numerics.LinearAlgebra; using MathNet.Numerics.LinearAlgebra.Double; namespace Core.Common.Base.Geometry { /// /// This class contains general mathematical routines for 2D lines. /// public static class Math2D { /// /// Constant which is used to precision errors in comparisons. /// private const double epsilonForComparisons = 1e-6; /// /// Splits the line geometry at given lengths. /// /// The line to split. /// The lengths where the splits should be placed. /// A sequence of line geometries of N elements long where N is the number /// of elements in . /// Thrown when the sum of all elements in /// does not fully cover the line given by - or - when /// contains negative values - or - when /// does not have 2 or more elements. public static Point2D[][] SplitLineAtLengths(IEnumerable linePoints, double[] lengths) { if (lengths.Any(l => l < 0)) { throw new ArgumentException(Resources.Math2D_SplitLineAtLengths_All_lengths_cannot_be_negative, nameof(lengths)); } if (linePoints.Count() <= 1) { throw new ArgumentException(Resources.Math2D_SplitLineAtLengths_Not_enough_points_to_make_line, nameof(linePoints)); } Segment2D[] lineSegments = ConvertLinePointsToLineSegments(linePoints).ToArray(); if (Math.Abs(lengths.Sum(l => l) - lineSegments.Sum(s => s.Length)) > epsilonForComparisons) { throw new ArgumentException(Resources.Math2D_SplitLineAtLengths_Sum_of_lengths_must_equal_line_length, nameof(lengths)); } return SplitLineSegmentsAtLengths(lineSegments, lengths); } /// /// Creates an enumerator that converts a sequence of line points to a sequence of line segments. /// /// The line points. /// A sequence of N elements, where N is the number of elements in /// - 1, or 0 if only has one or no elements. public static IEnumerable ConvertLinePointsToLineSegments(IEnumerable linePoints) { Point2D endPoint = null; foreach (Point2D linePoint in linePoints) { Point2D startPoint = endPoint; endPoint = linePoint; if (startPoint != null) { yield return new Segment2D(startPoint, endPoint); } } } /// /// Determines the intersection point of a line which passes through the and /// the ; and a line which passes through the /// and the . /// /// A which the first line passes through. /// Another which the first line passes through. /// A which the second line passes through. /// Another which the second line passes through. /// An with coordinates at the point where the lines intersect. Or null when no /// intersection point exists (lines are parallel). /// /// Taken from: https://www.topcoder.com/community/data-science/data-science-tutorials/geometry-concepts-line-intersection-and-its-applications/ /// Based on https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection /// /// Thrown when , , /// or is null. /// Thrown when equals or /// equals , which makes it impossible to determine /// a line through the points. public static Point2D LineIntersectionWithLine(Point2D line1Point1, Point2D line1Point2, Point2D line2Point1, Point2D line2Point2) { if (line1Point1 == null) { throw new ArgumentNullException(nameof(line1Point1)); } if (line1Point2 == null) { throw new ArgumentNullException(nameof(line1Point2)); } if (line2Point1 == null) { throw new ArgumentNullException(nameof(line2Point1)); } if (line2Point2 == null) { throw new ArgumentNullException(nameof(line2Point2)); } if (line1Point1.Equals(line1Point2) || line2Point1.Equals(line2Point2)) { throw new ArgumentException(Resources.Math2D_LineIntersectionWithLine_Line_points_are_equal); } double aLine = line1Point2.Y - line1Point1.Y; double bLine = line1Point1.X - line1Point2.X; double cLine = aLine * line1Point1.X + bLine * line1Point1.Y; double aOtherLine = line2Point2.Y - line2Point1.Y; double bOtherLine = line2Point1.X - line2Point2.X; double cOtherLine = aOtherLine * line2Point1.X + bOtherLine * line2Point1.Y; double determinant = aLine * bOtherLine - aOtherLine * bLine; if (Math.Abs(determinant) < epsilonForComparisons) { return null; } double x = (bOtherLine * cLine - bLine * cOtherLine) / determinant; double y = (aLine * cOtherLine - aOtherLine * cLine) / determinant; return new Point2D(x, y); } /// /// Determines if two points are equal. /// /// The first point. /// The second point. /// True when the points are equal. False otherwise. /// Thrown when or /// is null. public static bool AreEqualPoints(Point2D point1, Point2D point2) { if (point1 == null) { throw new ArgumentNullException(nameof(point1)); } if (point2 == null) { throw new ArgumentNullException(nameof(point2)); } return Math.Abs(point1.X - point2.X) < epsilonForComparisons && Math.Abs(point1.Y - point2.Y) < epsilonForComparisons; } /// /// Determines the intersection points of a of with a vertical line /// which is plotted at x=. /// /// A collection of segments that possibly intersect with the /// vertical line at x=. /// The X-coordinate of the vertical line. /// A of with all intersection points of the /// with the vertical line at x=. /// Thrown when is null. /// Segments which have length=0 or which are vertical, will not return an intersection point. public static IEnumerable SegmentsIntersectionWithVerticalLine(IEnumerable segments, double verticalLineX) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } var intersectionPointY = new Collection(); foreach (Segment2D segment in segments.Where(s => s.ContainsX(verticalLineX))) { Point2D intersectionPoint = LineIntersectionWithVerticalLine(segment.FirstPoint, segment.SecondPoint, verticalLineX); if (intersectionPoint != null) { intersectionPointY.Add(intersectionPoint); } } return intersectionPointY; } /// /// Calculates the length of a line defined as a collection of . /// /// The points that make up a 2D line. /// The sum of the distances between consecutive points. /// Thrown when is null. public static double Length(IEnumerable points) { if (points == null) { throw new ArgumentNullException(nameof(points)); } double length = 0; Point2D previousPoint = null; foreach (Point2D point in points) { if (previousPoint != null) { length += previousPoint.GetEuclideanDistanceTo(point); } previousPoint = point; } return length; } /// /// Calculates the intersection between two 2D segments. /// /// The first 2D segment. /// The second 2D segment. /// The intersection calculation result. /// Thrown when or /// is null. /// Implementation from http://geomalgorithms.com/a05-_intersect-1.html /// based on method intersect2D_2Segments. public static Segment2DIntersectSegment2DResult GetIntersectionBetweenSegments(Segment2D segment1, Segment2D segment2) { if (segment1 == null) { throw new ArgumentNullException(nameof(segment1)); } if (segment2 == null) { throw new ArgumentNullException(nameof(segment2)); } Vector u = segment1.SecondPoint - segment1.FirstPoint; Vector v = segment2.SecondPoint - segment2.FirstPoint; Vector w = segment1.FirstPoint - segment2.FirstPoint; double d = PerpDotProduct(u, v); if (Math.Abs(d) < epsilonForComparisons) { // Segments can be considered parallel... if (AreCollinear(u, v, w)) { // ... and collinear ... if (IsSegmentAsPointIntersectionDegenerateScenario(segment1, segment2)) { // ... but either or both segments are point degenerates: return HandleSegmentAsPointIntersectionDegenerates(segment1, segment2); } // ... so there is a possibility of overlapping or connected lines: return HandleCollinearSegmentIntersection(segment1, segment2, v, w); } // ... but not collinear, so no intersection possible: return Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } // Segments are at an angle and may intersect: double sI = PerpDotProduct(v, w) / d; if (sI < 0.0 || sI > 1.0) { return Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } double tI = PerpDotProduct(u, w) / d; if (tI < 0.0 || tI > 1.0) { return Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } Point2D intersectionPoint = segment1.FirstPoint + u.Multiply(sI); return Segment2DIntersectSegment2DResult.CreateIntersectionResult(intersectionPoint); } /// /// Gets the interpolated point between the two end points of the at /// the of the length from the first end point. /// /// The segment to interpolate over. /// The fraction of the length of the segment where to obtain a new point. /// A new at the interpolated point. /// Thrown when is null. public static Point2D GetInterpolatedPointAtFraction(Segment2D lineSegment, double fraction) { if (lineSegment == null) { throw new ArgumentNullException(nameof(lineSegment)); } if (double.IsNaN(fraction) || fraction < 0.0 || fraction > 1.0) { throw new ArgumentOutOfRangeException(nameof(fraction), @"Fraction needs to be defined in range [0.0, 1.0] in order to reliably interpolate."); } Vector segmentVector = lineSegment.SecondPoint - lineSegment.FirstPoint; return lineSegment.FirstPoint + segmentVector.Multiply(fraction); } /// /// Determines if two parallel vectors are collinear. /// /// The first 2D vector. /// The second 2D vector. /// The vector from the tail of /// to the tail of . /// True if the vectors are collinear, false otherwise. private static bool AreCollinear(Vector vector1, Vector vector2, Vector tailsVector) { return Math.Abs(PerpDotProduct(vector1, tailsVector)) < epsilonForComparisons && Math.Abs(PerpDotProduct(vector2, tailsVector)) < epsilonForComparisons; } private static Segment2DIntersectSegment2DResult HandleCollinearSegmentIntersection(Segment2D segment1, Segment2D segment2, Vector v, Vector w) { double t0, t1; Vector w2 = segment1.SecondPoint - segment2.FirstPoint; if (v[0] != 0.0) { t0 = w[0] / v[0]; t1 = w2[0] / v[0]; } else { t0 = w[1] / v[1]; t1 = w2[1] / v[1]; } // Require t0 to be smaller than t1, swapping if needed: if (t0 > t1) { double tempSwapVariable = t0; t0 = t1; t1 = tempSwapVariable; } if (t0 > 1.0 || t1 < 0.0) { // There is no overlap: return Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } t0 = Math.Max(0.0, t0); t1 = Math.Min(1.0, t1); Point2D intersectionPoint1 = segment2.FirstPoint + v.Multiply(t0); if (Math.Abs(t0 - t1) < epsilonForComparisons) { // Segments intersect at a point: return Segment2DIntersectSegment2DResult.CreateIntersectionResult(intersectionPoint1); } // Segments overlap: Point2D intersectionPoint2 = segment2.FirstPoint + v.Multiply(t1); return Segment2DIntersectSegment2DResult.CreateOverlapResult(intersectionPoint1, intersectionPoint2); } private static bool IsSegmentAsPointIntersectionDegenerateScenario(Segment2D segment1, Segment2D segment2) { return IsSegmentActuallyPointDegenerate(segment1) || IsSegmentActuallyPointDegenerate(segment2); } private static bool IsSegmentActuallyPointDegenerate(Segment2D segment) { return segment.Length < epsilonForComparisons; } private static Segment2DIntersectSegment2DResult HandleSegmentAsPointIntersectionDegenerates(Segment2D segment1, Segment2D segment2) { bool segment1IsPointDegenerate = IsSegmentActuallyPointDegenerate(segment1); bool segment2IsPointDegenerate = IsSegmentActuallyPointDegenerate(segment2); if (segment1IsPointDegenerate) { if (segment2IsPointDegenerate) { // Both segments can be considered Point2D return segment1.FirstPoint.Equals(segment2.FirstPoint) ? Segment2DIntersectSegment2DResult.CreateIntersectionResult(segment1.FirstPoint) : Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } { return IsPointInCollinearSegment(segment1.FirstPoint, segment2) ? Segment2DIntersectSegment2DResult.CreateIntersectionResult(segment1.FirstPoint) : Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } } return IsPointInCollinearSegment(segment2.FirstPoint, segment1) ? Segment2DIntersectSegment2DResult.CreateIntersectionResult(segment2.FirstPoint) : Segment2DIntersectSegment2DResult.CreateNoIntersectResult(); } private static bool IsPointInCollinearSegment(Point2D point, Segment2D colinearSegment) { if (colinearSegment.IsVertical()) { double minY = Math.Min(colinearSegment.FirstPoint.Y, colinearSegment.SecondPoint.Y); double maxY = Math.Max(colinearSegment.FirstPoint.Y, colinearSegment.SecondPoint.Y); return minY <= point.Y && point.Y <= maxY; } double minX = Math.Min(colinearSegment.FirstPoint.X, colinearSegment.SecondPoint.X); double maxX = Math.Max(colinearSegment.FirstPoint.X, colinearSegment.SecondPoint.X); return minX <= point.X && point.X <= maxX; } /// /// Performs the dot product between a vector and a perpendicularized vector. /// /// The vector. /// The vector that will be made perpendicular before doing /// the dot product operation. /// The dot product between a vector and a perpendicularized vector. private static double PerpDotProduct(Vector vector1, Vector vector2) { Vector perpendicularVectorForVector2 = ToPerpendicular(vector2); return vector1.DotProduct(perpendicularVectorForVector2); } /// /// Creates a new based on a vector that is perpendicular. /// /// The vector. /// A vector of the same length as . private static Vector ToPerpendicular(Vector vector) { return new DenseVector(new[] { -vector[1], vector[0] }); } private static Point2D[][] SplitLineSegmentsAtLengths(Segment2D[] lineSegments, double[] lengths) { var splitResults = new Point2D[lengths.Length][]; var index = 0; double lineSegmentRemainder = lineSegments[index].Length; double distanceOnSegment = 0; Point2D startPoint = lineSegments[index].FirstPoint; for (var i = 0; i < lengths.Length; i++) { double splitDistanceRemainder = lengths[i]; var subLine = new List { startPoint }; while (splitDistanceRemainder > lineSegmentRemainder) { splitDistanceRemainder -= lineSegmentRemainder; subLine.Add(lineSegments[index].SecondPoint); if (index < lineSegments.Length - 1) { lineSegmentRemainder = lineSegments[++index].Length; distanceOnSegment = 0; } } if (i < lengths.Length - 1) { Point2D interpolatedPoint = GetInterpolatedPoint(lineSegments[index], distanceOnSegment + splitDistanceRemainder); subLine.Add(interpolatedPoint); distanceOnSegment += splitDistanceRemainder; lineSegmentRemainder -= splitDistanceRemainder; startPoint = interpolatedPoint; } else { EnsureLastSplitResultHasEndPointOfLine(subLine, lineSegments); } splitResults[i] = subLine.ToArray(); } return splitResults; } private static void EnsureLastSplitResultHasEndPointOfLine(ICollection subLine, IList lineSegments) { int lastSegmentIndex = lineSegments.Count - 1; if (!subLine.Contains(lineSegments[lastSegmentIndex].SecondPoint)) { subLine.Add(lineSegments[lastSegmentIndex].SecondPoint); } } private static Point2D GetInterpolatedPoint(Segment2D lineSegment, double splitDistance) { double interpolationFactor = splitDistance / lineSegment.Length; return GetInterpolatedPointAtFraction(lineSegment, interpolationFactor); } /// /// Determines the intersection point of a line through the points and /// with a vertical line at x=. If /// equals , then no intersection point /// will be returned. /// /// A which the line passes through. /// Another which the line passes through. /// The X-coordinate of the vertical line. /// The intersection point between the line through and /// and the vertical line at x=; or null if /// the line through and is vertical or /// the points are equal. private static Point2D LineIntersectionWithVerticalLine(Point2D point1, Point2D point2, double x) { var verticalLineFirstPoint = new Point2D(x, 0); var verticalLineSecondPoint = new Point2D(x, 1); try { return LineIntersectionWithLine(point1, point2, verticalLineFirstPoint, verticalLineSecondPoint); } catch (ArgumentException) { return null; } } } }