diff --git a/src/Itinero/Algorithms/Collections/BinaryHeap.cs b/src/Itinero/Algorithms/Collections/BinaryHeap.cs
index 053e939e..5adb9a13 100644
--- a/src/Itinero/Algorithms/Collections/BinaryHeap.cs
+++ b/src/Itinero/Algorithms/Collections/BinaryHeap.cs
@@ -60,7 +60,7 @@ public int Count
}
///
- /// Enqueues a given item.
+ /// Enqueues a given item. The lower the value, the higher the priority.
///
public void Push(T item, float priority)
{
diff --git a/src/Itinero/LocalGeo/Coordinate.cs b/src/Itinero/LocalGeo/Coordinate.cs
index db924b1a..22400855 100644
--- a/src/Itinero/LocalGeo/Coordinate.cs
+++ b/src/Itinero/LocalGeo/Coordinate.cs
@@ -17,6 +17,7 @@
*/
using System;
+using System.Resources;
using Itinero.Navigation.Directions;
namespace Itinero.LocalGeo
@@ -72,34 +73,36 @@ public Coordinate OffsetWithDirection(float distance, DirectionEnum direction)
var oldLat = this.Latitude.ToRadians();
var oldLon = this.Longitude.ToRadians();
- var bearing = ((double)(int)direction).ToRadians();
+ var bearing = ((double) (int) direction).ToRadians();
var newLatitude = System.Math.Asin(
- System.Math.Sin(oldLat) *
- System.Math.Cos(ratioInRadians) +
- System.Math.Cos(oldLat) *
- System.Math.Sin(ratioInRadians) *
- System.Math.Cos(bearing));
+ System.Math.Sin(oldLat) *
+ System.Math.Cos(ratioInRadians) +
+ System.Math.Cos(oldLat) *
+ System.Math.Sin(ratioInRadians) *
+ System.Math.Cos(bearing));
var newLongitude = oldLon + System.Math.Atan2(
- System.Math.Sin(bearing) *
- System.Math.Sin(ratioInRadians) *
- System.Math.Cos(oldLat),
- System.Math.Cos(ratioInRadians) -
- System.Math.Sin(oldLat) *
- System.Math.Sin(newLatitude));
-
+ System.Math.Sin(bearing) *
+ System.Math.Sin(ratioInRadians) *
+ System.Math.Cos(oldLat),
+ System.Math.Cos(ratioInRadians) -
+ System.Math.Sin(oldLat) *
+ System.Math.Sin(newLatitude));
+
var newLat = newLatitude.ToDegrees();
if (newLat > 180)
{
newLat = newLat - 360;
}
+
var newLon = newLongitude.ToDegrees();
if (newLon > 180)
{
newLon = newLon - 360;
}
- return new Coordinate((float)newLat, (float)newLon);
+
+ return new Coordinate((float) newLat, (float) newLon);
}
///
@@ -116,7 +119,8 @@ public static float DistanceEstimateInMeter(Coordinate coordinate1, Coordinate c
/// Returns an estimate of the distance between the two given coordinates.
///
/// Accuraccy decreases with distance.
- public static float DistanceEstimateInMeter(float latitude1, float longitude1, float latitude2, float longitude2)
+ public static float DistanceEstimateInMeter(float latitude1, float longitude1, float latitude2,
+ float longitude2)
{
var lat1Rad = (latitude1 / 180d) * System.Math.PI;
var lon1Rad = (longitude1 / 180d) * System.Math.PI;
@@ -128,7 +132,7 @@ public static float DistanceEstimateInMeter(float latitude1, float longitude1, f
var m = System.Math.Sqrt(x * x + y * y) * RadiusOfEarth;
- return (float)m;
+ return (float) m;
}
///
@@ -137,14 +141,26 @@ public static float DistanceEstimateInMeter(float latitude1, float longitude1, f
public static float DistanceEstimateInMeter(System.Collections.Generic.List coordinates)
{
var length = 0f;
- for(var i = 1; i < coordinates.Count; i++)
+ for (var i = 1; i < coordinates.Count; i++)
{
length += Coordinate.DistanceEstimateInMeter(coordinates[i - 1].Latitude, coordinates[i - 1].Longitude,
coordinates[i].Latitude, coordinates[i].Longitude);
}
+
return length;
}
+ ///
+ /// Return how much degrees the coordinate is away from b
+ ///
+ ///
+ ///
+ public float DistanceInDegrees(Coordinate b)
+ {
+ return (float) Math.Sqrt((Latitude - b.Latitude) * (Latitude - b.Latitude) +
+ ((Longitude - b.Longitude) * (Longitude - b.Longitude)));
+ }
+
///
/// Offsets this coordinate with a given distance.
///
@@ -160,7 +176,7 @@ public Coordinate OffsetWithDistances(float meter)
return new Coordinate(this.Latitude + (meter / latDistance) * 0.1f,
this.Longitude + (meter / lonDistance) * 0.1f);
}
-
+
///
/// Returns a description of this object.
///
@@ -168,10 +184,49 @@ public override string ToString()
{
if (this.Elevation.HasValue)
{
- return string.Format("{0},{1}@{2}m", this.Latitude.ToInvariantString(), this.Longitude.ToInvariantString(),
+ return string.Format("{0},{1}@{2}m", this.Latitude.ToInvariantString(),
+ this.Longitude.ToInvariantString(),
this.Elevation.Value.ToInvariantString());
}
+
return string.Format("{0},{1}", this.Latitude.ToInvariantString(), this.Longitude.ToInvariantString());
}
+
+ public static Coordinate operator +(Coordinate a, Coordinate b)
+ {
+ return new Coordinate(){Latitude = a.Latitude + b.Latitude, Longitude = a.Longitude + b.Longitude};;
+ }
+
+ public static Coordinate operator -(Coordinate a, Coordinate b)
+ {
+ return new Coordinate() {Latitude = a.Latitude - b.Latitude, Longitude = a.Longitude - b.Longitude};
+ }
+
+ public static Coordinate operator /(Coordinate a, float b)
+ {
+ return new Coordinate(a.Latitude / b, a.Longitude / b);
+ }
+
+ public static float DotProduct(Coordinate a, Coordinate b)
+ {
+ return a.Latitude * b.Latitude + a.Longitude * b.Longitude;
+ }
+/*
+ public override bool Equals(object obj)
+ {
+ var coor = obj as Coordinate?;
+ if (coor == null)
+ {
+ return false;
+ }
+
+ var c = (Coordinate) coor;
+ return this.Latitude == c.Latitude && this.Longitude == c.Longitude;
+ }
+
+ public override int GetHashCode()
+ {
+ return Latitude.GetHashCode() + Longitude.GetHashCode();
+ }*/
}
}
\ No newline at end of file
diff --git a/src/Itinero/LocalGeo/Extensions.cs b/src/Itinero/LocalGeo/Extensions.cs
index b165ebc8..87cca88e 100644
--- a/src/Itinero/LocalGeo/Extensions.cs
+++ b/src/Itinero/LocalGeo/Extensions.cs
@@ -20,6 +20,7 @@
using System.Collections.Generic;
using System.Linq;
using Itinero.LocalGeo.Operations;
+using Itinero.Profiles.Lua.Tree.Statements;
namespace Itinero.LocalGeo
{
@@ -78,6 +79,7 @@ public static List Simplify(this List shape, float epsil
return simplified;
}
}
+
return shape;
}
@@ -88,7 +90,8 @@ public static List Simplify(this List shape, float epsil
/// Epsilon.
/// First.
/// Last.
- public static List SimplifyBetween(this List points, float epsilonInMeter, int first, int last)
+ public static List SimplifyBetween(this List points, float epsilonInMeter, int first,
+ int last)
{
if (points == null)
throw new ArgumentNullException(nameof(points));
@@ -118,7 +121,8 @@ public static List SimplifyBetween(this List points, flo
}
if (foundIndex > 0 && maxDistance > epsilonInMeter)
- { // a point was found and it is far enough.
+ {
+ // a point was found and it is far enough.
var before = SimplifyBetween(points, epsilonInMeter, first, foundIndex);
var after = SimplifyBetween(points, epsilonInMeter, foundIndex, last);
@@ -128,14 +132,17 @@ public static List SimplifyBetween(this List points, flo
{
result.Add(before[idx]);
}
+
for (int idx = 0; idx < after.Count; idx++)
{
result.Add(after[idx]);
}
+
return result;
}
}
- return new List(new Coordinate[] { points[first], points[last] });
+
+ return new List(new Coordinate[] {points[first], points[last]});
}
///
@@ -175,7 +182,8 @@ public static Coordinate[] SimplifyBetween(this Coordinate[] points, float epsil
}
if (foundIndex > 0 && maxDistance > epsilonInMeter)
- { // a point was found and it is far enough.
+ {
+ // a point was found and it is far enough.
var before = SimplifyBetween(points, epsilonInMeter, first, foundIndex);
var after = SimplifyBetween(points, epsilonInMeter, foundIndex, last);
@@ -185,14 +193,17 @@ public static Coordinate[] SimplifyBetween(this Coordinate[] points, float epsil
{
result[idx] = before[idx];
}
+
for (int idx = 0; idx < after.Length; idx++)
{
result[idx + before.Length - 1] = after[idx];
}
+
return result;
}
}
- return new Coordinate[] { points[first], points[last] };
+
+ return new Coordinate[] {points[first], points[last]};
}
///
@@ -209,7 +220,7 @@ public static Polygon ToPolygon(this Box box)
new Coordinate(box.MaxLat, box.MaxLon),
new Coordinate(box.MinLat, box.MaxLon),
new Coordinate(box.MinLat, box.MinLon)
- })
+ })
};
}
@@ -217,7 +228,8 @@ public static Polygon ToPolygon(this Box box)
/// Calculates a bounding box for the polygon.
/// TODO: if this is needed a lot, we should cache it in the polygon, see https://github.com/itinero/routing/issues/138
///
- public static void BoundingBox(this Polygon polygon, out float north, out float east, out float south, out float west)
+ public static void BoundingBox(this Polygon polygon, out float north, out float east, out float south,
+ out float west)
{
polygon.ExteriorRing.BoundingBox(out north, out east, out south, out west);
}
@@ -225,7 +237,8 @@ public static void BoundingBox(this Polygon polygon, out float north, out float
///
/// Calculates a bounding box for the ring.
///
- public static void BoundingBox(this List exteriorRing, out float north, out float east, out float south, out float west)
+ public static void BoundingBox(this List exteriorRing, out float north, out float east,
+ out float south, out float west)
{
PointInPolygon.BoundingBox(exteriorRing, out north, out east, out south, out west);
}
@@ -256,6 +269,7 @@ public static Coordinate LocationAfterDistance(this Line line, float offset, boo
{
return LocationAfterDistance(line.Coordinate1, line.Coordinate2, offset);
}
+
return LocationAfterDistance(line.Coordinate2, line.Coordinate1, offset);
}
@@ -268,7 +282,8 @@ public static Coordinate LocationAfterDistance(this Line line, float offset, boo
///
public static Coordinate LocationAfterDistance(Coordinate coordinate1, Coordinate coordinate2, float offset)
{
- return LocationAfterDistance(coordinate1, coordinate2, Coordinate.DistanceEstimateInMeter(coordinate1, coordinate2),
+ return LocationAfterDistance(coordinate1, coordinate2,
+ Coordinate.DistanceEstimateInMeter(coordinate1, coordinate2),
offset);
}
@@ -280,7 +295,8 @@ public static Coordinate LocationAfterDistance(Coordinate coordinate1, Coordinat
/// The distance between the two, when we already calculated it before.
/// The offset in meter starting at coordinate1.
///
- public static Coordinate LocationAfterDistance(Coordinate coordinate1, Coordinate coordinate2, float distanceBetween, float offset)
+ public static Coordinate LocationAfterDistance(Coordinate coordinate1, Coordinate coordinate2,
+ float distanceBetween, float offset)
{
var ratio = offset / distanceBetween;
return new Coordinate(
@@ -312,7 +328,7 @@ public static IEnumerable Intersect(this Polygon polygon, float lati
///
public static bool PointIn(this Polygon poly, Coordinate point)
{
- return PointInPolygon.PointIn(poly, point);
+ return PointInPolygon.ContainsPoint(poly, point);
}
///
@@ -320,7 +336,7 @@ public static bool PointIn(this Polygon poly, Coordinate point)
///
public static bool PointIn(List ring, Coordinate point)
{
- return PointInPolygon.PointIn(ring, point);
+ return PointInPolygon.ContainsPoint(ring, point);
}
///
@@ -336,6 +352,17 @@ public static Polygon ConvexHull(this IEnumerable points)
};
}
+ public static bool IsClockwise(this Polygon p)
+ {
+ return p.ExteriorRing.IsClockwise();
+ }
+
+ public static bool IsClockwise(this List p)
+ {
+ return p.SignedSurfaceArea() > 0;
+ }
+
+
///
/// Calculates the area of the polygon
///
@@ -351,5 +378,104 @@ public static float SurfaceArea(this Polygon poly)
return poly.ExteriorRing.SurfaceArea() - internalArea;
}
+
+ public static List IntersectionsWith(this Polygon a, Polygon b)
+ {
+ if (!a.IsClockwise())
+ {
+ a.ExteriorRing.Reverse();
+ }
+
+ if (!b.IsClockwise())
+ {
+ b.ExteriorRing.Reverse();
+ }
+
+ return a.Intersect(b, out var union);
+ }
+
+ public static Polygon UnionWith(this Polygon a, Polygon b)
+ {
+ if (!a.IsClockwise())
+ {
+ a.ExteriorRing.Reverse();
+ }
+
+ if (!b.IsClockwise())
+ {
+ b.ExteriorRing.Reverse();
+ }
+
+ a.Intersect(b, out var union);
+ return union;
+ }
+
+ public static List DifferencesWith(this Polygon a, Polygon b)
+ {
+ if (!a.IsClockwise())
+ {
+ a.ExteriorRing.Reverse();
+ }
+
+ if (!b.IsClockwise())
+ {
+ b.ExteriorRing.Reverse();
+ }
+
+ return a.Intersect(b, out var union, true);
+ }
+
+ public static Coordinate NorthernMost(this Polygon a)
+ {
+ var min = 0;
+ var northernMost = a.ExteriorRing[0];
+ for (var i = 1; i < a.ExteriorRing.Count - 1; i++)
+ {
+ var coor = a.ExteriorRing[i];
+ if (northernMost.Latitude == coor.Latitude)
+ {
+ if (northernMost.Longitude > coor.Longitude)
+ {
+ northernMost = coor;
+ min = i;
+ }
+ }
+ else if (northernMost.Latitude < coor.Latitude)
+ {
+ northernMost = coor;
+ min = i;
+ }
+ }
+
+ return northernMost;
+ }
+
+ ///
+ /// Will rotate the exterior ring so that 'i' is the new start (and end, in case of a closed polygon).
+ /// Result will always be closed
+ ///
+ ///
+ ///
+ public static void RotateExteriorRing(this Polygon a, int newStart)
+ {
+
+ if (a.IsClosed())
+ {
+ a.ExteriorRing.RemoveAt(a.ExteriorRing.Count);
+ }
+
+ var newExterior = new List();
+ for (var i = newStart; i < a.ExteriorRing.Count; i++)
+ {
+ newExterior.Add(a.ExteriorRing[i]);
+ }
+
+ for (var i = 0; i < newStart; i++)
+ {
+ newExterior.Add(a.ExteriorRing[i]);
+ }
+ newExterior.Add(newExterior[0]);
+ a.ExteriorRing = newExterior;
+ }
}
}
\ No newline at end of file
diff --git a/src/Itinero/LocalGeo/Line.cs b/src/Itinero/LocalGeo/Line.cs
index 5946f876..9d4de90b 100644
--- a/src/Itinero/LocalGeo/Line.cs
+++ b/src/Itinero/LocalGeo/Line.cs
@@ -16,6 +16,10 @@
* limitations under the License.
*/
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
namespace Itinero.LocalGeo
{
///
@@ -41,10 +45,7 @@ public Line(Coordinate coordinate1, Coordinate coordinate2)
///
public float A
{
- get
- {
- return _coordinate2.Latitude - _coordinate1.Latitude;
- }
+ get { return _coordinate2.Latitude - _coordinate1.Latitude; }
}
///
@@ -52,10 +53,7 @@ public float A
///
public float B
{
- get
- {
- return _coordinate1.Longitude - _coordinate2.Longitude;
- }
+ get { return _coordinate1.Longitude - _coordinate2.Longitude; }
}
///
@@ -63,10 +61,7 @@ public float B
///
public float C
{
- get
- {
- return this.A * _coordinate1.Longitude + this.B * _coordinate1.Latitude;
- }
+ get { return this.A * _coordinate1.Longitude + this.B * _coordinate1.Latitude; }
}
///
@@ -74,23 +69,18 @@ public float C
///
public Coordinate Coordinate1
{
- get
- {
- return _coordinate1;
- }
+ get { return _coordinate1; }
}
-
+
///
/// Gets the second coordinate.
///
public Coordinate Coordinate2
{
- get
- {
- return _coordinate2;
- }
+ get { return _coordinate2; }
}
+
///
/// Gets the middle of this line.
///
@@ -110,12 +100,10 @@ public Coordinate Middle
///
public float Length
{
- get
- {
- return Coordinate.DistanceEstimateInMeter(_coordinate1, _coordinate2);
- }
+ get { return Coordinate.DistanceEstimateInMeter(_coordinate1, _coordinate2); }
}
+
///
/// Calculates the intersection point of the given line with this line.
///
@@ -123,20 +111,55 @@ public float Length
///
/// Assumes the given line is not a segment and this line is a segment.
///
- public Coordinate? Intersect(Line line)
+ public Coordinate? Intersect(Line l2, bool useBoundingBoxChecks = true)
{
- var det = (double)(line.A * this.B - this.A * line.B);
- if (System.Math.Abs(det) <= E)
- { // lines are parallel; no intersections.
+ // We get two lines and try to calculate the intersection between them
+ // We are only interested in intersections that are actually between the two coordinates
+ // This is quickly checked with bounding boxes
+ // In order to do this, we do some normalization of the lines first
+
+ var l1 = this.Normalize();
+ l2 = l2.Normalize();
+
+ if (useBoundingBoxChecks && (l1.MinLon() - l2.MaxLon() > E|| l1.MaxLon() - l2.MinLon() < E))
+ {
+ // No intersection possible
return null;
}
- else
- { // lines are not the same and not parallel so they will intersect.
- double x = (this.B * line.C - line.B * this.C) / det;
- double y = (line.A * this.C - this.A * line.C) / det;
- var coordinate = new Coordinate((float)y, (float)x);
+ if (useBoundingBoxChecks && (l1.MinLat() - l2.MaxLat() > E || l1.MaxLat() - l2.MinLat() < E))
+ {
+ // No intersection possible
+ return null;
+ }
+
+
+ var det = (double) (l2.A * l1.B - l1.A * l2.B);
+ if (Math.Abs(det) <= E)
+ {
+ // lines are parallel; no intersections.
+ return null;
+ }
+
+
+ // lines are not the same and not parallel so they will intersect.
+ var x = (l1.B * l2.C - l2.B * l1.C) / det;
+ var y = (l2.A * l1.C - l1.A * l2.C) / det;
+
+ // We have a coordinate!
+ var coordinate = new Coordinate((float) y, (float) x);
+
+
+ // It the point the within the bounding box of both lines?
+ if (useBoundingBoxChecks && !(l1.InBBox(coordinate) && l2.InBBox(coordinate)))
+ {
+ return null;
+ }
+
+ if (!useBoundingBoxChecks)
+ {
+ // Keep the old behaviour
// check if the coordinate is on this line.
var dist = this.A * this.A + this.B * this.B;
var line1 = new Line(coordinate, _coordinate1);
@@ -151,39 +174,63 @@ public float Length
{
return null;
}
+ }
- if (_coordinate1.Elevation.HasValue && _coordinate2.Elevation.HasValue)
- {
- if (_coordinate1.Elevation == _coordinate2.Elevation)
- { // don't calculate anything, elevation is identical.
- coordinate.Elevation = _coordinate1.Elevation;
- }
- else if (System.Math.Abs(this.A) < E && System.Math.Abs(this.B) < E)
- { // tiny segment, not stable to calculate offset
- coordinate.Elevation = _coordinate1.Elevation;
- }
- else
- { // calculate offset and calculate an estimiate of the elevation.
- if (System.Math.Abs(this.A) > System.Math.Abs(this.B))
- {
- var diffLat = System.Math.Abs(this.A);
- var diffLatIntersection = System.Math.Abs(coordinate.Latitude - _coordinate1.Latitude);
-
- coordinate.Elevation = (short)((_coordinate2.Elevation - _coordinate1.Elevation) * (diffLatIntersection / diffLat) +
- _coordinate1.Elevation);
- }
- else
- {
- var diffLon = System.Math.Abs(this.B);
- var diffLonIntersection = System.Math.Abs(coordinate.Longitude - _coordinate1.Longitude);
-
- coordinate.Elevation = (short)((_coordinate2.Elevation - _coordinate1.Elevation) * (diffLonIntersection / diffLon) +
- _coordinate1.Elevation);
- }
- }
- }
+ if (!l1.Coordinate1.Elevation.HasValue || !l1.Coordinate2.Elevation.HasValue)
+ {
+ // No elevation data. We are done
return coordinate;
}
+
+
+ // There is elevation data we have to take into account
+ if (l1.Coordinate1.Elevation == l2.Coordinate2.Elevation)
+ {
+ // don't calculate anything, elevation is identical.
+ coordinate.Elevation = l1.Coordinate1.Elevation;
+ return coordinate;
+ }
+
+ if (Math.Abs(l1.A) < E && Math.Abs(l1.B) < E)
+ {
+ // tiny segment, not stable enough to calculate offset
+ coordinate.Elevation = l1.Coordinate1.Elevation;
+ return coordinate;
+ }
+
+
+ // calculate offset and calculate an estimiate of the elevation.
+ if (Math.Abs(l1.A) > Math.Abs(l1.B))
+ {
+ var diffLat = Math.Abs(l1.A);
+ var diffLatIntersection = Math.Abs(coordinate.Latitude - l1.Coordinate1.Latitude);
+
+ coordinate.Elevation =
+ (short) ((l1.Coordinate2.Elevation - l1.Coordinate1.Elevation) *
+ (diffLatIntersection / diffLat) +
+ l1.Coordinate1.Elevation);
+ }
+ else
+ {
+ var diffLon = System.Math.Abs(l1.B);
+ var diffLonIntersection = Math.Abs(coordinate.Longitude - l1.Coordinate1.Longitude);
+
+ coordinate.Elevation =
+ (short) ((l1.Coordinate2.Elevation - l1.Coordinate1.Elevation) *
+ (diffLonIntersection / diffLon) +
+ l1.Coordinate1.Elevation);
+ }
+
+ return coordinate;
+ }
+
+ ///
+ /// Calculates the slope number of this line (richtingscoëfficient)
+ ///
+ ///
+ public float Slope()
+ {
+ return (Coordinate1.Latitude - Coordinate2.Latitude) / (Coordinate1.Longitude - Coordinate2.Longitude);
}
///
@@ -192,20 +239,20 @@ public float Length
public Coordinate? ProjectOn(Coordinate coordinate)
{
if (this.Length < E)
- {
+ {
return null;
}
// get direction vector.
- var diffLat = ((double)_coordinate2.Latitude - (double)_coordinate1.Latitude) * 100.0;
- var diffLon = ((double)_coordinate2.Longitude - (double)_coordinate1.Longitude) * 100.0;
+ var diffLat = ((double) _coordinate2.Latitude - (double) _coordinate1.Latitude) * 100.0;
+ var diffLon = ((double) _coordinate2.Longitude - (double) _coordinate1.Longitude) * 100.0;
// increase this line in length if needed.
var thisLine = this;
if (this.Length < 50)
{
- thisLine = new Line(_coordinate1, new Coordinate((float)(diffLat + coordinate.Latitude),
- (float)(diffLon + coordinate.Longitude)));
+ thisLine = new Line(_coordinate1, new Coordinate((float) (diffLat + coordinate.Latitude),
+ (float) (diffLon + coordinate.Longitude)));
}
// rotate 90°.
@@ -214,19 +261,21 @@ public float Length
diffLat = temp;
// create second point from the given coordinate.
- var second = new Coordinate((float)(diffLat + coordinate.Latitude), (float)(diffLon + coordinate.Longitude));
+ var second = new Coordinate((float) (diffLat + coordinate.Latitude),
+ (float) (diffLon + coordinate.Longitude));
// create a second line.
var line = new Line(coordinate, second);
// calculate intersection.
- var projected = thisLine.Intersect(line);
+ var projected = thisLine.Intersect(line, false);
// check if coordinate is on this line.
if (!projected.HasValue)
{
return null;
}
+
if (!thisLine.Equals(this))
{
// check if the coordinate is on this line.
@@ -237,14 +286,17 @@ public float Length
{
return null;
}
+
var line2 = new Line(projected.Value, _coordinate2);
var distTo2 = line2.A * line2.A + line2.B * line2.B;
if (distTo2 > dist)
{
return null;
}
+
return projected;
}
+
return projected;
}
@@ -258,7 +310,103 @@ public float Length
{
return Coordinate.DistanceEstimateInMeter(coordinate, projected.Value);
}
+
return null;
}
+
+ ///
+ /// Returns a line where the coordinate with the lowest Latitude will be saved as Coordinate1
+ /// The line might be a fresh clone of a pointer to this.
+ ///
+ ///
+ public Line OrderedLatitude()
+ {
+ return Coordinate1.Latitude < Coordinate2.Latitude ? this : new Line(Coordinate2, Coordinate1);
+ }
+
+ ///
+ /// Returns a line where the coordinate with the lowest Longitude will be saved as Coordinate1
+ /// The line might be a fresh clone of a pointer to this.
+ ///
+ ///
+ public Line OrderedLongitude()
+ {
+ return Coordinate1.Longitude < Coordinate2.Longitude ? this : new Line(Coordinate2, Coordinate1);
+ }
+
+ ///
+ /// If (and only if) this line crossed the ante-meridian, the line segment is normalized.
+ /// This is done by taking the negative coordinate and adding a 360 degrees to it.
+ /// We assume that a line crosses the antemeridian if abs(longitude) are both more then 90°
+ /// This will construct a new Line if normalization happens, or return 'this' if the coordinate is well behaved.
+ ///
+ /// Note: the lowest longitude will always be coordinate1 afterwards; an OrderedLongitude is called as well
+ ///
+ ///
+ public Line Normalize()
+ {
+ var c1 = Coordinate1.Longitude;
+ var c2 = Coordinate2.Longitude;
+ if (Math.Sign(c1) != Math.Sign(c2) &&
+ Math.Abs(c1) > 90 && Math.Abs(c2) > 90)
+ {
+ // We cross the ante-meridian. We 'normalize'
+ // Which one is the culprit?
+ return c1 < 0
+ ? new Line(Coordinate2, Coordinate1 + new Coordinate(0, 360))
+ : new Line(Coordinate1, Coordinate2 + new Coordinate(0, 360));
+ }
+
+ return this.OrderedLongitude();
+ }
+
+ public float MaxLat()
+ {
+ return Math.Max(Coordinate1.Latitude, Coordinate2.Latitude);
+ }
+
+ public float MinLat()
+ {
+ return Math.Min(Coordinate1.Latitude, Coordinate2.Latitude);
+ }
+
+ public float MaxLon()
+ {
+ return Math.Max(Coordinate1.Longitude, Coordinate2.Longitude);
+ }
+
+ public float MinLon()
+ {
+ return Math.Min(Coordinate1.Longitude, Coordinate2.Longitude);
+ }
+
+ public bool InBBox(Coordinate c)
+ {
+ return MinLat() <= c.Latitude && c.Latitude <= MaxLat() && MinLon() <= c.Longitude && c.Longitude <= MaxLon();
+ }
+
+ ///
+ ///
+ /// Comparer which compares the latitudes of Coordinate1 of both lines
+ ///
+ public class Latitude1Comparer : IComparer
+ {
+ public int Compare(Line x, Line y)
+ {
+ return x.Coordinate1.Latitude.CompareTo(y.Coordinate1.Latitude);
+ }
+ }
+
+ ///
+ ///
+ /// Comparer which compares the longitudes of Coordinate1 of both lines
+ ///
+ public class Longitude1Comparer : IComparer
+ {
+ public int Compare(Line x, Line y)
+ {
+ return x.Coordinate1.Longitude.CompareTo(y.Coordinate1.Longitude);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Itinero/LocalGeo/Operations/Intersections.cs b/src/Itinero/LocalGeo/Operations/Intersections.cs
index f3ab7379..6a7c7b2c 100644
--- a/src/Itinero/LocalGeo/Operations/Intersections.cs
+++ b/src/Itinero/LocalGeo/Operations/Intersections.cs
@@ -16,6 +16,7 @@
* limitations under the License.
*/
+using System;
using System.Collections.Generic;
namespace Itinero.LocalGeo.Operations
@@ -25,6 +26,9 @@ namespace Itinero.LocalGeo.Operations
///
internal static class Intersections
{
+
+
+
///
/// Returns intersection points with the given polygon.
///
@@ -36,10 +40,25 @@ internal static class Intersections
///
internal static IEnumerable Intersect(this Polygon polygon, float latitude1, float longitude1,
float latitude2, float longitude2)
+ {
+ var line = new Line(new Coordinate(latitude1, longitude1), new Coordinate(latitude2, longitude2));
+ return polygon.IntersectWithLines(line);
+ }
+
+
+ ///
+ /// Returns intersection points with the given polygon.
+ ///
+ /// The polygon
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static IEnumerable IntersectWithLines(this Polygon polygon, Line line)
{
var E = 0.001f; // this is 1mm.
- var line = new Line(new Coordinate(latitude1, longitude1), new Coordinate(latitude2, longitude2));
// REMARK: yes, this can be way way faster but it's only used in preprocessing.
// use a real geo library like NTS if you want this faster or submit a pull request.
@@ -63,8 +82,8 @@ internal static IEnumerable Intersect(this Polygon polygon, float la
}
var previousDistance = 0f;
- var previous = new Coordinate(latitude1, longitude1);
- var previousInside = polygon.PointIn(previous);
+ var previous = line.Coordinate1;
+ var previousInside = polygon.ContainsPoint(previous);
var cleanIntersections = new List();
foreach (var intersection in sortedList)
@@ -78,7 +97,7 @@ internal static IEnumerable Intersect(this Polygon polygon, float la
// calculate in or out.
var middle = new Coordinate((previous.Latitude + intersection.Value.Latitude) / 2,
(previous.Longitude + intersection.Value.Longitude) / 2);
- var middleInside = polygon.PointIn(middle);
+ var middleInside = polygon.ContainsPoint(middle);
if (previousInside != middleInside ||
cleanIntersections.Count == 0)
{ // in or out change or this is the first intersection.
@@ -92,6 +111,12 @@ internal static IEnumerable Intersect(this Polygon polygon, float la
return cleanIntersections;
}
+ ///
+ /// Returns the intersection points of a ring and a line
+ ///
+ ///
+ ///
+ ///
private static IEnumerable> IntersectInternal(this List ring,
Line line)
{
diff --git a/src/Itinero/LocalGeo/Operations/PointInPolygon.cs b/src/Itinero/LocalGeo/Operations/PointInPolygon.cs
index 669f45f2..b1c005cd 100644
--- a/src/Itinero/LocalGeo/Operations/PointInPolygon.cs
+++ b/src/Itinero/LocalGeo/Operations/PointInPolygon.cs
@@ -32,11 +32,11 @@ internal static class PointInPolygon
/// Note that polygons spanning a pole, without a point at the pole itself, will fail to detect points within the polygon;
/// (e.g. Polygon=[(lat=80°, 0), (80, 90), (80, 180)] will *not* detect the point (85, 90))
///
- internal static bool PointIn(this Polygon poly, Coordinate point)
+ internal static bool ContainsPoint(this Polygon poly, Coordinate point)
{
// For startes, the point should lie within the outer
- var inOuter = PointIn(poly.ExteriorRing, point);
+ var inOuter = ContainsPoint(poly.ExteriorRing, point);
if (!inOuter)
{
return false;
@@ -45,7 +45,7 @@ internal static bool PointIn(this Polygon poly, Coordinate point)
// and it should *not* lay within any inner ring
for (var i = 0; i < poly.InteriorRings.Count; i++)
{
- var inInner = PointIn(poly.InteriorRings[i], point);
+ var inInner = ContainsPoint(poly.InteriorRings[i], point);
if (inInner)
{
return false;
@@ -57,7 +57,7 @@ internal static bool PointIn(this Polygon poly, Coordinate point)
///
/// Returns true if the given point lies within the ring.
///
- internal static bool PointIn(List ring, Coordinate point)
+ internal static bool ContainsPoint(List ring, Coordinate point)
{
// Coordinate of the point. Longitude might be changed in the antemeridian-crossing case
@@ -106,7 +106,7 @@ internal static bool PointIn(List ring, Coordinate point)
// no intersections passed yet -> not within the polygon
var result = false;
- for (int i = 0; i < ring.Count; i++)
+ for (var i = 0; i < ring.Count; i++)
{
var start = ring[i];
var end = ring[(i + 1) % ring.Count];
@@ -144,7 +144,7 @@ internal static bool PointIn(List ring, Coordinate point)
// Analogously, at least one point of the segments should be on the right (east) of the point;
// otherwise, no intersection is possible (as the raycast goes right)
- if (!(Math.Max(stLong, endLong) >= longitude))
+ if (!(Math.Max(stLong, endLong) > longitude))
{
continue;
}
diff --git a/src/Itinero/LocalGeo/Operations/PolygonAreaCalcutor.cs b/src/Itinero/LocalGeo/Operations/PolygonAreaCalcutor.cs
index a445f514..ddd3d537 100644
--- a/src/Itinero/LocalGeo/Operations/PolygonAreaCalcutor.cs
+++ b/src/Itinero/LocalGeo/Operations/PolygonAreaCalcutor.cs
@@ -12,23 +12,30 @@ namespace Itinero.LocalGeo.Operations
internal static class PolygonAreaCalcutor
{
///
- /// Calculates the surface area of a closed, not-self-intersecting polygon
+ /// Calculates the surface area of a closed, not-self-intersecting polygon.
+ /// Will return a negative result if the polygon is counterclockwise
///
///
///
- internal static float SurfaceArea(this List points)
+ public static float SignedSurfaceArea(this List points)
{
var l = points.Count;
var area = 0f;
for (var i = 1; i < l+1; i++)
{
- var p = points[i % l];
- var pi = points[(i + 1) % l];
- var pm = points[(i - 1)];
- area += p.Longitude * (pi.Latitude - pm.Latitude);
+ var cur = points[i % l];
+ var nxt = points[(i + 1) % l];
+ var prev = points[(i - 1)];
+ area += cur.Longitude * (prev.Latitude - nxt.Latitude);
}
- return Math.Abs(area / 2);
+ return area / 2;
}
+
+ public static float SurfaceArea(this List points)
+ {
+ return Math.Abs(points.SignedSurfaceArea());
+ }
+
}
}
\ No newline at end of file
diff --git a/src/Itinero/LocalGeo/Operations/PolygonIntersection.cs b/src/Itinero/LocalGeo/Operations/PolygonIntersection.cs
new file mode 100644
index 00000000..debb4afd
--- /dev/null
+++ b/src/Itinero/LocalGeo/Operations/PolygonIntersection.cs
@@ -0,0 +1,329 @@
+using System;
+using System.Collections.Generic;
+
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Itinero.Test.LocalGeo.Operations")]
+namespace Itinero.LocalGeo.Operations
+{
+ public static class PolygonIntersection
+ {
+ ///
+ /// Produces an intersection of the given two polygons.
+ /// Polygons should be closed and saved in a clockwise fashion.
+ ///
+ /// For now, interior rings are note supported as well
+ ///
+ /// The first polygon
+ /// The second polygon
+ /// The out parameter, which will contain the union ring afterwards. Might be null
+ /// Set to true if you want the differences between the polygons instead of the intersecion
+ internal static List Intersect(this Polygon a, Polygon b, out Polygon union, bool differences = false)
+ {
+ // yes, this is not a state of the art algorithm.
+ if (!a.IsClockwise() || !b.IsClockwise())
+ {
+ throw new InvalidOperationException(
+ "Polygons should be constructed in a clockwise fasion for intersections");
+ }
+
+ if (a.InteriorRings.Count != 0 || b.InteriorRings.Count != 0)
+ {
+ throw new InvalidOperationException("Polygons with holes are not supported for intersections");
+ }
+
+ if (differences)
+ {
+ // Walking is suddenly inversed
+ a.ExteriorRing.Reverse();
+ }
+
+ var result = new List();
+ union = null;
+
+ // quick bbox test
+ a.BoundingBox(out var na, out var ea, out var sa, out var wa);
+ b.BoundingBox(out var nb, out var eb, out var sb, out var wb);
+
+ if (na < sb || nb < sa || ea < wb || eb < wa)
+ {
+ // bounding boxes do not overlap - no overlap is possible at all
+ return result;
+ }
+ // How are the intersecting polygons calculated?
+ // Calculate the intersecting points, and the indices of the lines in each polygon
+ // Take an interecting point, and start walking from it, following the line of polygon a
+ // (This assumes polygons are saved clockwise)
+ // When anohter intersection point is met, we swap to following polygon b
+ // (we follow the clockwise direction again)
+
+ // We continue this until we reach the starting point again, building a new polygon while we go
+ // Plot twist: the newly built polygon should be clockwise as well. If not, it is a hole between the polygons
+
+
+ var intersections = IntersectionsBetween(a.ExteriorRing, b.ExteriorRing);
+
+ PrepareIntersectionMatrix(intersections, out var aIntersections, out var bIntersections);
+
+
+ // Bounding box of polygons a | b
+ var n = Math.Max(na, nb);
+ var e = Math.Max(ea, eb);
+ var s = Math.Min(sa, sb);
+ var w = Math.Min(wa, wb);
+
+ // We keep track of the northern-most point of each polygon
+ // This is a trick to keep out duplicate polygons
+ var excludedPoints = new HashSet();
+
+ foreach (var intersection in intersections)
+ {
+ var p = WalkIntersection(intersection, aIntersections, bIntersections, a, b);
+ if (!p.IsClockwise()) continue; // The polygon is a hole between a & b
+
+ // One polygon will also be the result of walking along the outer edges of both a & b
+ // In other words, the union of the polygons
+ // We just take the bbox of the polygon. If this polygon encompasses both other polygons, we're pretty sure its the union
+ p.BoundingBox(out var pn, out var pe, out var ps, out var pw);
+ var poly = new Polygon() {ExteriorRing = p};
+ if (pn == n && pe == e && ps == s && pw == w)
+ {
+ // This is the union polygon.
+ union = poly;
+ }
+ else
+ {
+ var northernMost = poly.NorthernMost();
+ if (excludedPoints.Contains(northernMost))
+ {
+ // This polygon has already been discovered previously
+ continue;
+ }
+ excludedPoints.Add(northernMost);
+ result.Add(poly);
+ }
+ }
+
+ return result;
+ }
+
+ internal static void PrepareIntersectionMatrix(List> intersections,
+ out Dictionary> aIntersections,
+ out Dictionary> bIntersections)
+ {
+ aIntersections = new Dictionary>();
+ bIntersections = new Dictionary>();
+
+
+ // Preprocessing of the intersections: build an easily accessible data structure, bit of a sparse matrix
+ foreach (var intersection in intersections)
+ {
+ var i = intersection.Item1;
+ var j = intersection.Item2;
+ var coor = intersection.Item3;
+ if (!aIntersections.ContainsKey(i))
+ {
+ aIntersections[i] = new SortedList();
+ }
+
+ if (!bIntersections.ContainsKey(j))
+ {
+ bIntersections[j] = new SortedList();
+ }
+
+ aIntersections[i][j] = coor;
+ bIntersections[j][i] = coor;
+ }
+ }
+
+
+ internal static List WalkIntersection(Tuple intersection,
+ Dictionary> aIntersections,
+ Dictionary> bIntersections,
+ Polygon a, Polygon b)
+ {
+ var newPolygon = new List();
+
+ var curPolyIsA = true;
+
+
+ var startI = intersection.Item1;
+ var startJ = intersection.Item2;
+
+ var i = startI;
+ var j = startJ;
+ do
+ {
+ if (curPolyIsA)
+ {
+ var result = FollowAlong(newPolygon, a, i, j, aIntersections);
+
+ i = result.Item1;
+ j = result.Item2;
+
+ curPolyIsA = false;
+ }
+ else
+ {
+ var result = FollowAlong(newPolygon, b, j, i, bIntersections);
+ j = result.Item1; // Beware: i and j are swapped here
+ i = result.Item2;
+
+ curPolyIsA = true;
+ }
+ } while (!(startI == i && startJ == j));
+
+ if (!newPolygon[0].Equals(newPolygon[newPolygon.Count - 1]))
+ {
+ newPolygon.Add(newPolygon[0]);
+ }
+
+ return newPolygon;
+ }
+
+ ///
+ /// Follows a polygon until another intersection is met
+ ///
+ /// The segment causing the intersection in the passed polygon (in the passed matrix)
+ /// The segment causing the intersection in the other polygon
+ internal static Tuple FollowAlong(List route, Polygon a, int i, int j,
+ Dictionary> aIntersections)
+ {
+ if (i == 0 && j == 1)
+ {
+ i = i;
+ }
+
+ // We start at given intersection point. This will be part of the route
+ route.Add(aIntersections[i][j]);
+
+
+ // Then, we check if the intersection point we landed on is the only intersection
+ if (aIntersections[i].Keys.Count != 1)
+ {
+ // Multiple intersections on the segment we have to walk
+ // We'd better calculate the direction in which we have to go
+ // We can't really make assumptions about clockwise/counterclockwise
+ // So: we take the point to which we are walking from the polygon
+ // And have a look to next/previous neighbour and their distances
+ var walkingTo = (i + 1) % a.ExteriorRing.Count;
+ var walkingToCoor = a.ExteriorRing[walkingTo];
+
+
+ var inters = aIntersections[i];
+ // We get both neighbours...
+ var n1 = inters.NeighbourOf(j);
+ var n2 = inters.PrevNeighbourOf(j);
+
+ var d1 = walkingToCoor.DistanceInDegrees(inters[n1]);
+ var d2 = walkingToCoor.DistanceInDegrees(inters[n2]);
+
+ var n = n1;
+ var d = d1;
+ // ...and we get the neighbour closest to the edge point
+ if (d2 < d1)
+ {
+ n = n2;
+ d = d2;
+ }
+
+
+ // We have found the closest neighbour
+ // One special edge case remains: what if the current intersection point (i,j) is closer to the point we are walking to then the closest neighbour?
+ // For this, we check that the new intersection point is closer to our distance
+ if (d < walkingToCoor.DistanceInDegrees(inters[j]))
+ {
+ return Tuple.Create (i, n);
+ }
+ // If not, we just continue with walking along our polygon... (The loop below, out of the if)
+ }
+
+
+
+ while (true)
+ {
+ // No intersection for the rest of this segment
+ // The next point is the element following on the startIndex
+ i = (i + 1) % (a.ExteriorRing.Count - 1); // -1 to avoid the point closing the element
+
+ var coor = a.ExteriorRing[i];
+ route.Add(coor);
+
+
+ // Does this new line i intersect?
+
+ // if no intersections. Just continue the loop until we get to one
+ if (!aIntersections.ContainsKey(i)) continue;
+
+
+ // yes, there are intersections
+ // How do we figure out what intersection is the next we'll see?
+ // As we come from a fresh start, we can only get to a 'fresh' intersection
+ // Meaning that we can only get to the collision with the highest or lowest collision index
+ // Funilly, as both polygons are both clockwise, the highest collision number can aways be taken ...
+ // ... except if they both pass each other (e.g. two rectangulars)
+ // So we simply take the one that is closest by
+
+ var intersections = aIntersections[i];
+ var j0 = intersections.Keys[0];
+ var j1 = intersections.Keys[intersections.Count - 1];
+
+ j = j0;
+
+ var d0 = intersections[j0].DistanceInDegrees(coor);
+ var d1 = intersections[j1].DistanceInDegrees(coor);
+
+ if (d1 < d0)
+ {
+ j = j1;
+ }
+
+ return Tuple.Create(i, j);
+ }
+ }
+
+ internal static TKey NeighbourOf(this SortedList list, TKey k)
+ {
+ return list.Keys[(list.IndexOfKey(k) + 1) % list.Count];
+ }
+
+ internal static TKey PrevNeighbourOf(this SortedList list, TKey k)
+ {
+ var key = list.IndexOfKey(k) - 1;
+ if (key < 0)
+ {
+ key = list.Keys.Count - 1;
+ }
+
+ return list.Keys[key];
+ }
+
+
+ ///
+ /// Compares al lines from ring0 and ring2, gives a list of intersection points and segments back.
+ /// Assumes closed rings
+ ///
+ ///
+ ///
+ /// A list of intersections, plus the lines which caused the intersection. Elements from ring0 will always be Item0, whereas elements from ring1 will always be Item1
+ public static List> IntersectionsBetween(List ring0,
+ List ring1)
+ {
+ var intersects = new List>();
+ for (var i = 0; i < ring0.Count - 1; i++)
+ {
+ var l0 = new Line(ring0[i], ring0[i + 1]);
+ for (var j = 0; j < ring1.Count - 1; j++)
+ {
+ var l1 = new Line(ring1[j], ring1[j + 1]);
+ var coor = l0.Intersect(l1);
+ if (coor == null) continue;
+
+ intersects.Add(Tuple.Create(i, j, (Coordinate) coor));
+ }
+ }
+
+ return intersects;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Itinero/LocalGeo/Operations/QuickHull.cs b/src/Itinero/LocalGeo/Operations/QuickHull.cs
index 7342b2d0..b966c596 100644
--- a/src/Itinero/LocalGeo/Operations/QuickHull.cs
+++ b/src/Itinero/LocalGeo/Operations/QuickHull.cs
@@ -63,7 +63,7 @@ internal static List RemoveFromHull(HashSet allPoints, L
///
internal static int UpdateHull(this List hull, Coordinate newPoint, bool inOrder = true)
{
- if (PointInPolygon.PointIn(
+ if (PointInPolygon.ContainsPoint(
hull, newPoint))
{
// Point is neatly contained within the polygon; nothing to do here
diff --git a/src/Itinero/LocalGeo/Polygon.cs b/src/Itinero/LocalGeo/Polygon.cs
index 1d9462ba..bd40b980 100644
--- a/src/Itinero/LocalGeo/Polygon.cs
+++ b/src/Itinero/LocalGeo/Polygon.cs
@@ -36,7 +36,17 @@ public class Polygon
public List> InteriorRings { get; set; } = new List>();
+ public bool IsClosed()
+ {
+ return ExteriorRing[0].Equals(ExteriorRing[ExteriorRing.Count - 1]);
+ }
-
+ public void MakeClosed()
+ {
+ if (!IsClosed())
+ {
+ ExteriorRing.Add(ExteriorRing[0]);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/test/Itinero.Test/Itinero.Test.csproj b/test/Itinero.Test/Itinero.Test.csproj
index 51edfd9b..95856921 100644
--- a/test/Itinero.Test/Itinero.Test.csproj
+++ b/test/Itinero.Test/Itinero.Test.csproj
@@ -51,6 +51,13 @@
+
+
+
+
+
+
+
@@ -67,6 +74,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Itinero.Test/LocalGeo/CoordinateTests.cs b/test/Itinero.Test/LocalGeo/CoordinateTests.cs
index 3a086d13..a3216426 100644
--- a/test/Itinero.Test/LocalGeo/CoordinateTests.cs
+++ b/test/Itinero.Test/LocalGeo/CoordinateTests.cs
@@ -84,5 +84,13 @@ public void TestGeoCoordinateOffsetEstimate()
Assert.AreEqual(distance, distanceLat, 0.3);
Assert.AreEqual(distance, distanceLon, 0.3);
}
+
+ [Test]
+ public void TestOps()
+ {
+ Assert.AreEqual(new Coordinate(10, 20), new Coordinate(50, 20) - new Coordinate(40, 0));
+ Assert.AreEqual(new Coordinate(10, 20), new Coordinate(10, 40) - new Coordinate(0, 20));
+ Assert.AreEqual(new Coordinate(10, 20), new Coordinate(50, 40) - new Coordinate(40, 20));
+ }
}
}
\ No newline at end of file
diff --git a/test/Itinero.Test/LocalGeo/ExtensionTests.cs b/test/Itinero.Test/LocalGeo/ExtensionTests.cs
index 981022ab..f2d1e2fe 100644
--- a/test/Itinero.Test/LocalGeo/ExtensionTests.cs
+++ b/test/Itinero.Test/LocalGeo/ExtensionTests.cs
@@ -52,7 +52,7 @@ public void TestSimplify()
Assert.AreEqual(shape[0].Longitude, simplified[0].Longitude);
Assert.AreEqual(shape[shape.Length - 1].Latitude, simplified[simplified.Length - 1].Latitude);
Assert.AreEqual(shape[shape.Length - 1].Longitude, simplified[simplified.Length - 1].Longitude);
-
+
simplified = shape.Simplify(0.0000001f);
Assert.IsNotNull(simplified);
Assert.AreEqual(5, simplified.Length);
@@ -87,7 +87,7 @@ public void TestLocationAfterDistance()
Assert.AreEqual(250, Coordinate.DistanceEstimateInMeter(location1, location), E);
Assert.AreEqual(total - 250, Coordinate.DistanceEstimateInMeter(location2, location), E);
}
-
+
///
/// A real-world convex-hull test.
///
@@ -97,7 +97,7 @@ public void TestData1()
var coors = "Itinero.Test.test_data.points.points1.geojson".LoadAsStream().LoadTestPoints();
var hull = coors.Convexhull();
- var hullGeoJson = new Polygon(){ExteriorRing = hull}.ToGeoJson();
+ var hullGeoJson = new Polygon() {ExteriorRing = hull}.ToGeoJson();
var expected = "Itinero.Test.test_data.points.points1.hull.geojson".LoadAsStream().ReadToEnd();
Assert.AreEqual(expected, hullGeoJson);
}
@@ -107,11 +107,12 @@ public void TestData1()
///
[Test]
public void TestData2()
- { var coors = "Itinero.Test.test_data.points.points2.geojson".LoadAsStream().LoadTestPoints();
+ {
+ var coors = "Itinero.Test.test_data.points.points2.geojson".LoadAsStream().LoadTestPoints();
var hull = coors.Convexhull();
- var hullGeoJson = new Polygon(){ExteriorRing = hull}.ToGeoJson();
- // System.IO.File.WriteAllText("/home/pietervdvn/Desktop/Result.geojson", hullGeoJson);
+ var hullGeoJson = new Polygon() {ExteriorRing = hull}.ToGeoJson();
+ // System.IO.File.WriteAllText("/home/pietervdvn/Desktop/Result.geojson", hullGeoJson);
var expected = "Itinero.Test.test_data.points.points2.hull.geojson".LoadAsStream().ReadToEnd();
Assert.AreEqual(expected, hullGeoJson);
}
diff --git a/test/Itinero.Test/LocalGeo/Operations/PolygonIntersectionTest.cs b/test/Itinero.Test/LocalGeo/Operations/PolygonIntersectionTest.cs
new file mode 100644
index 00000000..783e9c9f
--- /dev/null
+++ b/test/Itinero.Test/LocalGeo/Operations/PolygonIntersectionTest.cs
@@ -0,0 +1,130 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+using Itinero.LocalGeo;
+using Itinero.LocalGeo.IO;
+using Itinero.LocalGeo.Operations;
+using NUnit.Framework;
+
+namespace Itinero.Test.LocalGeo.Operations
+{
+ [TestFixture]
+ public class PolygonIntersectionTest
+ {
+ [Test]
+ public void TestIntersectionsBetween()
+ {
+ var a = "polygons.polygon1.geojson".LoadTestStream().LoadTestPolygon();
+ Assert.IsTrue(a.IsClockwise());
+ Assert.IsTrue(a.ExteriorRing[0].Equals(a.ExteriorRing.Last()));
+ var b = "polygons.polygon2.geojson".LoadTestStream().LoadTestPolygon();
+ Assert.IsFalse(b.IsClockwise());
+ var intersections = PolygonIntersection.IntersectionsBetween(a.ExteriorRing, b.ExteriorRing);
+
+
+ var expected =
+ new List("polygons.Intersections_Polygon1_Polygon2.geojson".LoadTestStream()
+ .LoadTestPoints());
+ for (var i = 0; i < intersections.Count; i++)
+ {
+ Assert.AreEqual(intersections[i].Item3.ToString(), expected[i].ToString());
+ }
+ }
+
+
+ [Test]
+ public void TestClockwise()
+ {
+ var a = "polygons.polygon1.geojson".LoadTestStream().LoadTestPolygon();
+ Assert.IsTrue(a.IsClockwise());
+ Assert.IsTrue(a.ExteriorRing[0].Equals(a.ExteriorRing.Last())); // is closed
+ a.ExteriorRing.Reverse();
+ Assert.IsFalse(a.IsClockwise());
+ // B is constructed COUNTERclockwise
+ var b = "polygons.polygon2.geojson".LoadTestStream().LoadTestPolygon();
+ Assert.IsFalse(b.IsClockwise());
+ b.ExteriorRing.Reverse();
+ Assert.IsTrue(b.IsClockwise());
+ }
+
+ [Test]
+ public void TestSum()
+ {
+ var c = "polygons.polygon3.geojson".LoadTestStream().LoadTestPolygon();
+ var sa = c.ExteriorRing.SignedSurfaceArea();
+ Assert.IsTrue(sa > 0);
+ c.ExteriorRing.Reverse();
+ sa = c.ExteriorRing.SignedSurfaceArea();
+ Assert.IsFalse(sa > 0);
+ }
+
+ public void Test(string poly1, string poly2, string expInter, string expUnion, bool dump = false)
+ {
+ var a = poly1.LoadTestStream().LoadTestPolygon();
+ var b = poly2.LoadTestStream().LoadTestPolygon();
+ var c = a.IntersectionsWith(b);
+
+ if (!dump)
+ {
+ Assert.IsTrue(c.Count == 1);
+ var poly = c[0];
+ poly.MakeClosed();
+ var exp = expInter.LoadTestStream().LoadTestPolygon();
+ Assert.AreEqual(poly.ToGeoJson(), exp.ToGeoJson());
+ }
+ else
+ {
+ File.WriteAllText("/home/pietervdvn/Desktop/Intersection.geojson", c.ToGeoJson());
+ }
+
+ var union = a.UnionWith(b);
+ union.MakeClosed();
+ if (!dump)
+ {
+ var exp = expUnion.LoadTestStream().LoadTestPolygon();
+ Assert.AreEqual(union.ToGeoJson(), exp.ToGeoJson());
+ }
+ else
+ {
+ File.WriteAllText("/home/pietervdvn/Desktop/Union.geojson", union.ToGeoJson());
+ }
+ }
+
+ [Test]
+ public void TestSimple()
+ {
+ Test("polygons.polygon1.geojson", "polygons.polygon2.geojson", "polygons.Intersect_1_2.geojson",
+ "polygons.Union1_2.geojson");
+ Test("polygons.polygon6.geojson", "polygons.polygon7.geojson", "polygons.Intersect_6_7.geojson",
+ "polygons.Union_6_7.geojson");
+ Test("polygons.polygon4.geojson", "polygons.polygon5.geojson", "polygons.Intersect_4_5.geojson",
+ "polygons.Union_4_5.geojson");
+ }
+
+ [Test]
+ public void TestInternals()
+ {
+ var a = "polygons.polygon6.geojson".LoadTestStream().LoadTestPolygon();
+ var b = "polygons.polygon7.geojson".LoadTestStream().LoadTestPolygon();
+
+
+ var intersections = PolygonIntersection.IntersectionsBetween(a.ExteriorRing, b.ExteriorRing);
+ PolygonIntersection.PrepareIntersectionMatrix(intersections, out var aInt,
+ out var bInt);
+
+ Assert.AreEqual(aInt[0][1], bInt[1][0]);
+
+ // What happens if we start walking clockwise, from the most northern intersection point on poly a?
+ // By chance the most northern point is also the first one
+
+ var visits = new List();
+ var nxtIntersection = PolygonIntersection.FollowAlong(visits, a, 0, 1, aInt);
+ Assert.AreEqual(nxtIntersection, Tuple.Create(1, 1));
+
+ var c = a.IntersectionsWith(b);
+
+ Assert.AreEqual(1, c.Count);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/RouteTests.cs b/test/Itinero.Test/RouteTests.cs
index 328b8cff..889e7d75 100644
--- a/test/Itinero.Test/RouteTests.cs
+++ b/test/Itinero.Test/RouteTests.cs
@@ -1001,6 +1001,7 @@ public void TestProjectOne()
Assert.IsTrue(time > route.ShapeMeta[1].Time);
Assert.IsTrue(time < route.ShapeMeta[2].Time);
+
Assert.IsTrue(route.ProjectOn(new Coordinate(51.26610064830449f, 4.801395535469055f), out projected, out shape, out distance, out time));
Assert.AreEqual(3, shape);
Assert.IsTrue(time > route.ShapeMeta[1].Time);
diff --git a/test/Itinero.Test/TestExtensions.cs b/test/Itinero.Test/TestExtensions.cs
index dbf3817f..0384c342 100644
--- a/test/Itinero.Test/TestExtensions.cs
+++ b/test/Itinero.Test/TestExtensions.cs
@@ -19,11 +19,12 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using Itinero.Data.Network;
using Itinero.LocalGeo;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
-using NetTopologySuite.IO;
+using Polygon = Itinero.LocalGeo.Polygon;
namespace Itinero.Test
{
@@ -77,6 +78,26 @@ public static IEnumerable LoadTestPoints(this Stream stream)
}
}
+ public static Polygon LoadTestPolygon(this Stream stream)
+ {
+ var points = new List(stream.LoadTestPoints());
+
+ if (!points.Last().Equals(points[0]))
+ {
+ // Polygon is not closed yet
+ points.Add(points[0]);
+ }
+
+ var poly = new Polygon() {ExteriorRing = points};
+
+ if (poly.ExteriorRing.Count <= 2)
+ {
+ throw new ArgumentException($"Too little elements in the exterior ring of the polygon (only {poly.ExteriorRing.Count} found)");
+ }
+ return poly;
+ }
+
+
///
/// Loads a test network from geojson.
///
@@ -91,7 +112,6 @@ private static IEnumerable LoadTestPoints(string geoJson)
if (point == null)
{
continue;
- ;
}
yield return new Coordinate((float) point.Coordinate.Y, (float) point.Coordinate.X);
@@ -107,5 +127,10 @@ public static Stream LoadAsStream(this string path)
return System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(
path);
}
+
+ public static Stream LoadTestStream(this string path)
+ {
+ return ("Itinero.Test.test_data." + path).LoadAsStream();
+ }
}
}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/points/Intersect_1_2.geojson b/test/Itinero.Test/test-data/points/Intersect_1_2.geojson
new file mode 100644
index 00000000..b2743020
--- /dev/null
+++ b/test/Itinero.Test/test-data/points/Intersect_1_2.geojson
@@ -0,0 +1,72 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [
+ 3.223501,
+ 51.21299
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.227663,
+ 51.2107
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.227577,
+ 51.20161
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.21705,
+ 51.20055
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.214703,
+ 51.20694
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.223501,
+ 51.21299
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/Itinero.Test/test-data/polygons/Intersect_1_2.geojson b/test/Itinero.Test/test-data/polygons/Intersect_1_2.geojson
new file mode 100644
index 00000000..b2743020
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Intersect_1_2.geojson
@@ -0,0 +1,72 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [
+ 3.223501,
+ 51.21299
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.227663,
+ 51.2107
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.227577,
+ 51.20161
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.21705,
+ 51.20055
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.214703,
+ 51.20694
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.223501,
+ 51.21299
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/Itinero.Test/test-data/polygons/Intersect_4_5.geojson b/test/Itinero.Test/test-data/polygons/Intersect_4_5.geojson
new file mode 100644
index 00000000..c0428728
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Intersect_4_5.geojson
@@ -0,0 +1,46 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [3.219033,51.21445]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223736,51.21441]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223773,51.20537]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.218349,51.20549]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.219033,51.21445]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/Intersect_6_7.geojson b/test/Itinero.Test/test-data/polygons/Intersect_6_7.geojson
new file mode 100644
index 00000000..6e8089c3
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Intersect_6_7.geojson
@@ -0,0 +1,38 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [3.225768,51.21384]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.232641,51.21038]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.225373,51.2076]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.225768,51.21384]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/Intersections_Polygon1_Polygon2.geojson b/test/Itinero.Test/test-data/polygons/Intersections_Polygon1_Polygon2.geojson
new file mode 100644
index 00000000..348ccd65
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Intersections_Polygon1_Polygon2.geojson
@@ -0,0 +1,27 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.223501,
+ 51.21299
+ ]
+ },
+ "properties": {}
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.21705,
+ 51.20055
+ ]
+ },
+ "properties": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/Union1_2.geojson b/test/Itinero.Test/test-data/polygons/Union1_2.geojson
new file mode 100644
index 00000000..88d264e6
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Union1_2.geojson
@@ -0,0 +1,94 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [3.21705,51.20055]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.212214,51.20005]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.202944,51.20866]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.215561,51.21737]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223501,51.21299]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.232899,51.21946]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.23307,51.2193]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.24646,51.20645]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.237963,51.19317]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.220196,51.19199]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.21705,51.20055]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/Union_4_5.geojson b/test/Itinero.Test/test-data/polygons/Union_4_5.geojson
new file mode 100644
index 00000000..8776410a
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Union_4_5.geojson
@@ -0,0 +1,110 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [3.218349,51.20549]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.210797,51.20567]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.210797,51.21452]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.219033,51.21445]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.219423,51.21955]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223715,51.21938]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223736,51.21441]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.237062,51.21428]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.237104,51.20506]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223773,51.20537]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.223801,51.19871]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.217835,51.19879]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.218349,51.20549]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/Union_6_7.geojson b/test/Itinero.Test/test-data/polygons/Union_6_7.geojson
new file mode 100644
index 00000000..8d14b5d9
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/Union_6_7.geojson
@@ -0,0 +1,78 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [3.225373,51.2076]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.211012,51.2021]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.212042,51.22076]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.225768,51.21384]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.226204,51.22076]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.241224,51.21968]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.23925,51.19747]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.224659,51.19629]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [3.225373,51.2076]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/polygon1.geojson b/test/Itinero.Test/test-data/polygons/polygon1.geojson
new file mode 100644
index 00000000..91e76672
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon1.geojson
@@ -0,0 +1,60 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ffcc00","name":"0"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2155609130859375,
+ 51.21736809218789
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ffaa00","name":"1"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.227663040161133,
+ 51.21070117641557
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ff8800","name":"2"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2275772094726562,
+ 51.201613260967655
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ff6600","name":"3"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.212213516235351,
+ 51.20005361592602
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ff4400","name":"4"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2029438018798824,
+ 51.20865789607864
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/Itinero.Test/test-data/polygons/polygon2.geojson b/test/Itinero.Test/test-data/polygons/polygon2.geojson
new file mode 100644
index 00000000..5cefd4b8
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon2.geojson
@@ -0,0 +1,82 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#00ffff","name":"0"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2328987121582027,
+ 51.21946474517965
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#00ddff","name":"1"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.214702606201172,
+ 51.2069371686342
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#00bbff","name":"2"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.220195770263672,
+ 51.19198564344851
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#0099ff","name":"3"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2379627227783203,
+ 51.19316903448836
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#0077ff","name":"4"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2464599609375,
+ 51.20645320245659
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#0055ff","name":"5"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2330703735351562,
+ 51.21930346757038
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#0033ff","name":"0 - 6"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2328987121582027,
+ 51.21946474517965
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/Itinero.Test/test-data/polygons/polygon3.geojson b/test/Itinero.Test/test-data/polygons/polygon3.geojson
new file mode 100644
index 00000000..cf5c34cb
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon3.geojson
@@ -0,0 +1,71 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2209682464599605,
+ 51.216964878742225
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.227105140686035,
+ 51.213470214285245
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.222427368164062,
+ 51.21140017256014
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2250022888183594,
+ 51.20889986821931
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.214273452758789,
+ 51.21129263538235
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2209682464599605,
+ 51.216964878742225
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/polygon4.geojson b/test/Itinero.Test/test-data/polygons/polygon4.geojson
new file mode 100644
index 00000000..a23bf95e
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon4.geojson
@@ -0,0 +1,49 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2107973098754883,
+ 51.21451864147378
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.237061500549316,
+ 51.21427669885685
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2371044158935547,
+ 51.20505504937601
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2107973098754883,
+ 51.20567346847335
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/polygon5.geojson b/test/Itinero.Test/test-data/polygons/polygon5.geojson
new file mode 100644
index 00000000..a2e8fe8c
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon5.geojson
@@ -0,0 +1,49 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.219423294067383,
+ 51.2195453837724
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.223714828491211,
+ 51.21938410644564
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2238006591796875,
+ 51.198709051956094
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2178354263305664,
+ 51.198789726900955
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Itinero.Test/test-data/polygons/polygon6.geojson b/test/Itinero.Test/test-data/polygons/polygon6.geojson
new file mode 100644
index 00000000..eae53ab0
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon6.geojson
@@ -0,0 +1,38 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ff0000"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2120418548583984,
+ 51.2207549457133
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ff0000"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2326412200927734,
+ 51.21037855923173
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"marker-color":"#ff0000"},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2110118865966797,
+ 51.202097278002434
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/Itinero.Test/test-data/polygons/polygon7.geojson b/test/Itinero.Test/test-data/polygons/polygon7.geojson
new file mode 100644
index 00000000..e77e3adc
--- /dev/null
+++ b/test/Itinero.Test/test-data/polygons/polygon7.geojson
@@ -0,0 +1,49 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2412242889404297,
+ 51.219679781113086
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2262039184570312,
+ 51.2207549457133
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.224658966064453,
+ 51.196288737916774
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.2392501831054683,
+ 51.19747201844406
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file