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