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