using System; using System.Collections.Generic; using System.Linq; using System.Text; using ThinkGeo.MapSuite.Core; using OCC600.Infrastructure.Dictionary.Utility.ExtensionMethods; using EMS.Infrastructure.Dictionary.Geography; using System.Collections.ObjectModel; using ThinkGeo.MapSuite.WpfDesktopEdition; using EMS.ThinkGeoLibrary.MapSuite.Utilities; namespace EMS.ThinkGeoLibrary.MapSuite.Projections { public class OffsetProjection : Projection, IDisposable { public static RectangleShape ESPG4326WorldExtent = new RectangleShape(-180, 90, 180, -90); /// /// Converts a given longitude to valid range as defined by ESPG 4326. To be modified to /// apply for any projection. /// /// /// public double ConstrainLongitude(double x) { if (x > ESPG4326WorldExtent.UpperRightPoint.X || x < ESPG4326WorldExtent.LowerLeftPoint.X) { if (x > 0) x = x - 360; else x = x + 360; x = ConstrainLongitude(x); } return x; } //TODO: synchronize these methods with ConvertLongitude protected override Vertex[] ConvertToExternalProjectionCore(double[] x, double[] y) { Vertex[] vertices = new Vertex[x.Length]; for (int i = 0; i < vertices.Length; i++) vertices[i] = new Vertex(x[i] - 360, y[i]); return vertices; } protected override Vertex[] ConvertToInternalProjectionCore(double[] x, double[] y) { Vertex[] vertices = new Vertex[x.Length]; for (int i = 0; i < vertices.Length; i++) vertices[i] = new Vertex(x[i] + 360, y[i]); return vertices; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { Close(); } #region Polygon Reprojections /// /// This method simply takes a polygon and returns a GeometryCollection /// that contains original polygon OuterRing and InnerRings. Note that outer ring is not /// passed through any of the reprojection routines implemented in this class. However /// each polygon inner ring is converted to a polygon and passed through the regular /// meridian crossing checks and corrected accordingly. /// /// /// public GeometryCollectionShape SeperateOuterAndInnerRings(PolygonShape polygon) { GeometryCollectionShape shape = new GeometryCollectionShape(); shape.Shapes.Add(new LineShape(polygon.OuterRing.Vertices)); // Added original polygon inners shape.Shapes.Add(new PolygonShape(polygon.OuterRing)); foreach (var innerRing in polygon.InnerRings) { var newPoly = new PolygonShape(innerRing); var correctedPoly = CorrectPolygon(newPoly, true).Treated; if (correctedPoly != null) { if (correctedPoly.GetWellKnownType() == WellKnownType.GeometryCollection) { foreach (var cshape in ((GeometryCollectionShape)correctedPoly).Shapes) shape.Shapes.Add(cshape); } else { shape.Shapes.Add(correctedPoly); } } } return shape; } public TreatedShape CorrectPolygon(PolygonShape polygon, bool correctOverDateline) { TreatedShape cgeom = null; var polyCrossings = polygon.OuterRing.CrossesMeridian(false); int crossingPoints = polyCrossings.Lines.Count; // if no crossing do, nothing if (crossingPoints == 0) //cgeom = new TreatedShape() { Treated = polygon, Original =polygon, Treatment = GeometryTreatment.None }; cgeom = new TreatedShape() { Treated = SeperateOuterAndInnerRings(polygon), Original =polygon, Treatment = GeometryTreatment.Decomposed }; else if (crossingPoints == 1) // we have two crossing points, so fill to edge cgeom = new TreatedShape() { Treated = FillPolygonCorrectlyToMapEdge(polygon, polyCrossings.Lines[0], true), Original = polygon, Treatment = GeometryTreatment.FilledToEdge }; else { if (correctOverDateline) // we have four crossing points, so split polygon cgeom = new TreatedShape() { Treated = SplitPolygonAt180Meridian(polygon, true), Original = polygon, Treatment = GeometryTreatment.WrappedAround180 }; else cgeom = new TreatedShape() { Treated = polygon, Original = polygon }; } return cgeom; } /// /// This function gets the MultipolygonShape so that the shape /// is displayed correctly on decimal degrees map /// split at the 180 degree of longitude. /// /// /// public MultipolygonShape SplitPolygonAt180Meridian(PolygonShape polygonShape) { Open(); //RectangleShape of the world used for the Intersection geometric function. var worldExtent = new RectangleShape(-180, 90, 180, -90); var westernRingShape = new RingShape(); var easternRingShape = new RingShape(); foreach (Vertex vertex in polygonShape.OuterRing.Vertices) { if (vertex.X > 0) { westernRingShape.Vertices.Add(ConvertToExternalProjection(vertex.X, vertex.Y)); easternRingShape.Vertices.Add(vertex); } else { westernRingShape.Vertices.Add(vertex); easternRingShape.Vertices.Add(ConvertToInternalProjection(vertex.X, vertex.Y)); } } Close(); var multiPolygonShape = new MultipolygonShape(); var westernPolygonShape = new PolygonShape(); var easternPolygonShape = new PolygonShape(); westernPolygonShape.OuterRing = westernRingShape; easternPolygonShape.OuterRing = easternRingShape; var westernMultiPolygonShape = worldExtent.GetIntersection(westernPolygonShape); var easternMultiPolygonShape = worldExtent.GetIntersection(easternPolygonShape); foreach (var polygonShape1 in westernMultiPolygonShape.Polygons) multiPolygonShape.Polygons.Add(polygonShape1); foreach (var polygonShape1 in easternMultiPolygonShape.Polygons) multiPolygonShape.Polygons.Add(polygonShape1); return multiPolygonShape; } /// /// When a polygon is split on the 180 boundary using method CreateSplitPolygonFeatures /// with allowForWrapping set to false and you pan E or W with WrapDatelineMode on, /// you see the two halves come together at the 180 boundary but with a line down the middle. /// If you pass allowWrapping as true, you get a GeometryCollection comprising the original polygon /// and its inner rings which you can style differently. /// public BaseShape SplitPolygonAt180Meridian(PolygonShape polygonShape, bool allowForWrapping) { if (!allowForWrapping) return SplitPolygonAt180Meridian(polygonShape); //for (int i = 0; i < polygonShape.OuterRing.Vertices.Count; i++) // Console.WriteLine(polygonShape.OuterRing.Vertices[i]); GeometryCollectionShape shape = new GeometryCollectionShape(); shape.Shapes.Add(CorrectLine180Longitude(new LineShape(polygonShape.OuterRing.Vertices))); shape.Shapes.Add(SplitPolygonAt180Meridian(polygonShape)); return shape; } #endregion /// /// This method takes in a line and checks if it crosses /// 180 longitude and adjust line accordingly. Method /// could return LineShape or MultiLineShape /// /// /// public BaseShape CorrectLine180Longitude(LineShape line) { BaseShape shape = null; if (line != null) { var meridianCrossings = line.CrossesMeridian(); int numOfCrossPoints = meridianCrossings.Lines.Count; if (numOfCrossPoints == 0) shape = line; else shape = SplitPointsAtCrossings(line.Vertices, meridianCrossings.Lines); } else shape = line; return shape; } /// /// This function gets the MultilineShape so that the line /// is displayed correctly on decimal degrees map /// split at the 180 degree of longitude. /// /// /// public MultilineShape SplitLine180Meridian(LineShape line) { Open(); var multiLineShape = new MultilineShape(); var westernLine = new LineShape(); var easternLine = new LineShape(); foreach (Vertex vertex in line.Vertices) { if (vertex.X > 0) { westernLine.Vertices.Add(ConvertToExternalProjection(vertex.X, vertex.Y)); easternLine.Vertices.Add(vertex); } else { westernLine.Vertices.Add(vertex); easternLine.Vertices.Add(ConvertToInternalProjection(vertex.X, vertex.Y)); } } Close(); // reproject east LineShape eastLine = null; if (easternLine.Vertices.Count > 0) { var crossingE = ESPG4326WorldExtent.GetCrossing(easternLine).Points; if (crossingE.Count > 0) eastLine = easternLine.GetLineOnALine(crossingE.First(), new PointShape(easternLine.Vertices.Last())) as LineShape; else eastLine = easternLine; } if (eastLine != null) { var cross = eastLine.CrossesMeridian(); if (cross != null && cross.Lines != null && cross.Lines.Count > 0) multiLineShape.Lines.AddRange(SplitLine180Meridian(eastLine).Lines); else multiLineShape.Lines.Add(eastLine); } // reproject west LineShape westLine = null; if (westernLine.Vertices.Count > 0) { var crossingW = ESPG4326WorldExtent.GetCrossing(westernLine).Points; if (crossingW.Count > 0) westLine = westernLine.GetLineOnALine(new PointShape(westernLine.Vertices.First()), crossingW.First()) as LineShape; else westLine = westernLine; } if (westLine != null) { var cross = westLine.CrossesMeridian(); if (cross != null && cross.Lines != null && cross.Lines.Count > 0) multiLineShape.Lines.AddRange(SplitLine180Meridian(westLine).Lines); else multiLineShape.Lines.Add(westLine); } return multiLineShape; } /// /// This method splits a LineShape that crosses at the various line crossings. /// Each line crossing is a straight line that comprises of two points only. /// /// original line to be split /// /// public MultilineShape SplitLineAtCrossings(LineShape line, IList crossings) { if (line == null) return null; return SplitPointsAtCrossings(line.Vertices, crossings); } /// /// Given a ringshape, split this into multiple lines at the following supplied crossings. /// /// original shape to be split /// crossings where we want line split /// public MultilineShape SplitRingAtCrossings(RingShape ring, IList crossings) { if (ring == null) return null; return SplitPointsAtCrossings(ring.Vertices, crossings); } public BaseShape FillPolygonCorrectlyToMapEdge(PolygonShape polygon, LineShape crossing, bool allowForWrapping) { var lastPoint = polygon.OuterRing.Vertices.Last(); var outerRing = FillRingToMapEdge(polygon.OuterRing, crossing); var filledPoly = new PolygonShape(outerRing, polygon.InnerRings); if (!allowForWrapping) return filledPoly; GeometryCollectionShape shape = new GeometryCollectionShape(); var _fOuterRing = FillRingToMapEdge(outerRing, crossing, true, true, lastPoint); shape.Shapes.Add(_fOuterRing); shape.Shapes.Add(filledPoly); return shape; } /// /// This method takes a ring that crosses the 180 boundary and distorts it /// to allow for map wrapping, so there is no line drawn across map and /// no ring edge on the 180 boundary. /// /// /// 180 boundary crossing /// /// true if ring should be filled toedge /// last point in ring /// public BaseShape FillRingToMapEdge(RingShape originalRing, LineShape crossing, bool allowForWRapping, bool ringFilledToEdge, Vertex lastPointInRing) { var mls = new MultilineShape(); var ring = (RingShape)originalRing.CloneDeep(); if (crossing == null || !crossing.Validate(ShapeValidationMode.Simple).IsValid) return ring; var crossP0 = crossing.Vertices[0]; var crossP1 = crossing.Vertices[1]; var p0 = ring.Vertices.IndexOf(ring.Vertices.FirstOrDefault(v => v == crossP0)); var p1 = ring.Vertices.IndexOf(ring.Vertices.FirstOrDefault(v => v == crossP1)); var y = 90; // between p0 and p1, add closest edge if (crossing.Vertices[0].Y < 0) y = -90; // we dont want to draw boundaries if wrapping is enabled, // so we split original closed outer ring into top and button sides // and extract corners if (allowForWRapping) { //PrintDebugMessage("p0 intersect", crossP0); //PrintDebugMessage("p1 intersect", crossP0); // if the crossing was at x=180, we probably // added a single vertex (180, 90) to fill to map edge int j = Math.Abs(crossing.Vertices[0].X) == 180 ? 1 : 2; int number2Remove = j + 1; int i = 0; // remove first corner for (; j < number2Remove; j++) { ring.Vertices.RemoveAt(p0 + j + i); --i; } j = Math.Abs(crossing.Vertices[1].X) == 180 ? 1 : 2; int tmp = 0; // remove second corners for (; j < number2Remove; j++) { tmp = p1 - j + i; if (tmp < 0) tmp = 0; ring.Vertices.RemoveAt(tmp); --i; } // cleanup resultant ring by doing following: // 1. Get distinct members // 2. Remove any vertices on 180/90 boundary var vertices = ring.Vertices.Distinct().ToArray(); ring.Vertices.Clear(); ring.Vertices.AddRange(vertices); // Console.WriteLine(Environment.NewLine + "after filling to edge"); for (i = 0; i < ring.Vertices.Count; i++) { if (Math.Abs(ring.Vertices[i].X) == 180 && Math.Abs(ring.Vertices[i].Y) == 90) { ring.Vertices.RemoveAt(i); i--; } //else // Console.WriteLine(ring.Vertices[i].ToString()); } // if there is still a crossing, rotate line, until we have no crossing var innerCrossing = new LineShape(ring.Vertices).CrossesMeridian(false); if (innerCrossing.Lines.Count > 0) { crossP0 = innerCrossing.Lines[0].Vertices[0]; crossP1 = innerCrossing.Lines[0].Vertices[1]; p0 = ring.Vertices.IndexOf(ring.Vertices.FirstOrDefault(v => v == crossP0)); p1 = ring.Vertices.IndexOf(ring.Vertices.FirstOrDefault(v => v == crossP1)); // if these are not start and end vertices, line will traverse map // to join the two points, so we re-arraange ring var _ring = new RingShape(); for (i = p1; i < ring.Vertices.Count; i++) _ring.Vertices.Add(ring.Vertices[i]); for (i = 0; i < p1; i++) _ring.Vertices.Add(ring.Vertices[i]); ring.Vertices.Clear(); ring.Vertices.AddRange(_ring.Vertices); } //Console.WriteLine(Environment.NewLine + "after filling to edge"); // add line at top of buttom of region mls.Lines.Add(new LineShape(new Vertex[] { new Vertex(ring.Vertices[0].X, y), new Vertex(ring.Vertices[ring.Vertices.Count - 1].X, y) })); } mls.Lines.Add(new LineShape(ring.Vertices)); return mls; } static void PrintDebugMessage(string text, params object[] value) { Console.WriteLine(string.Format(text, value)); } /// /// Here, we take a ring shape that traverses the 180 boundary and /// change it such that it fills to the map edge instead. This method does not allow for /// map wrapping (continuous panning) so there will be closing lines at the 180 boundaries /// and top or bottom edge of map. /// The steps involved are: /// 1. Get indices of points where ring intersects 180 boundary /// 2. Remove last closing point in ring /// 3. First, fill ring to +/-180 boundary, which is represented by vertices in crossing. For x coordinate /// on the 180 boundary, get exact intersection point on the 180 boudary and use its y coordinate. /// 4. Cleanup /// /// /// points on either side of 180 boundary crossing /// public RingShape FillRingToMapEdge(RingShape originalRing, LineShape crossing) { var ring = new RingShape(originalRing.Vertices); if (crossing == null || !crossing.Validate(ShapeValidationMode.Simple).IsValid) return ring; var crossP0 = crossing.Vertices[0]; var crossP1 = crossing.Vertices[1]; var p0 = ring.Vertices.IndexOf(ring.Vertices.FirstOrDefault(v => v == crossP0)); var p1 = ring.Vertices.IndexOf(ring.Vertices.FirstOrDefault(v => v == crossP1)); if (ring.Vertices[p0 + 1] == crossP1) p1 = p0 + 1; double y = 90; double x = 180; // between p0 and p1, add closest edge if (crossP0.Y < 0) y = -90; if (crossP0.X < 0) x = -180; // remove last closing point ring.Vertices.RemoveAt(ring.Vertices.Count - 1); int i = p0 + 1; // TODO. Refactor var sign = Math.Sign(crossP0.X); var cross180 = new Vertex(sign * 180, 0); var crossp1c = new Vertex(crossP1.X + (sign * 360), crossP1.Y); cross180.Y = Extensions.GetYOnLine(crossp1c.X, crossp1c.Y, crossP0.X, crossP0.Y, cross180.X); ring.Vertices.Insert(i, new Vertex(x, cross180.Y)); ring.Vertices.Insert(i + 1, new Vertex(x, y)); if (p1 > 0) { if (p1 == p0 + 1) i = p0 + 3; else i = p1 > 0 ? p1 - 1 : 0; } ring.Vertices.Insert(i, new Vertex(-x, y)); ring.Vertices.Insert(i + 1, new Vertex(-x, cross180.Y)); // reclose if (ring.Vertices.First() != ring.Vertices.Last()) ring.Vertices.Add(ring.Vertices.First()); if (ring.Validate(ShapeValidationMode.Simple).IsValid) return ring; else return originalRing; } /// /// This method splits a line given by list of points that crosses at the various line crossings. /// Each line crossing is a straight line that comprises of 2 vertices, which are the intersecting /// points on supplied line. /// /// original line points /// Each line crossing is a straight line that comprises of 2 vertices /// /// public MultilineShape SplitPointsAtCrossings(IList points, IList crossings) { crossings = crossings.Where(c => c != null && c.Vertices.Count >= 2 && c.Validate(ShapeValidationMode.Simple).IsValid).ToArray(); LineShape line = new LineShape(points); if (!line.Validate(ShapeValidationMode.Simple).IsValid || crossings == null || crossings.Count == 0) return new MultilineShape(new LineShape[] { line }); int idxToResplit = -1; IEnumerable lines = null; if (crossings.Count == 1) lines = SplitPointsAtCrossing(points, crossings[0], out idxToResplit); else { var _lines = new Collection(); IList subPoints = points; for (int i = 0; i < crossings.Count; i++) { var westAndEast = SplitPointsAtCrossing(subPoints, crossings[i], out idxToResplit); if (idxToResplit == 2) throw new ArgumentException(string.Format("Original geometry is not valid as both sides split from original line needs to be resplit.")); else if (idxToResplit == -1) { _lines.AddRange(westAndEast); break; } else { int idxNoReSplit = (idxToResplit + 1) % 2; _lines.Add(westAndEast[idxNoReSplit]); subPoints = westAndEast[idxToResplit].Vertices; } } lines = _lines; } if (lines != null && lines.Count() == 0) lines = new LineShape[] { line }; return new MultilineShape(lines); } /// /// This method takes a list of points splits this list on the /// provided crossing, returning a two element array with western line at index /// 0 and easter line at index 1. /// /// /// /// will specify index of line that needs to be resplit. -1 will be returned /// if neither line needs resplitting. 2 will be returned if both lines need resplitting, which typically is an error. /// public IList SplitPointsAtCrossing(IList points, LineShape crossing, out int indexNeedsResplitting) { indexNeedsResplitting = -1; var westernLine = new LineShape(); var easternLine = new LineShape(); int pi1 = points.IndexOf(points.FirstOrDefault(v => v == crossing.Vertices[0])); int pi2 = points.IndexOf(points.FirstOrDefault(v => v == crossing.Vertices[1])); Vertex p0 = new Vertex(); Vertex p1 = new Vertex(); for (int i = 0; i <= pi1; i++) { p0 = points[i]; p1 = points[i + 1]; if (i < pi1 - 1 && indexNeedsResplitting != -1) if (EMS.ThinkGeoLibrary.MapSuite.Utilities.MeridianTester.LineSegmentChangesDirection(p0, p1)) indexNeedsResplitting = 1; easternLine.Vertices.Add(p0); } bool eastLineNeedsResplitting = indexNeedsResplitting == 1; for (int i = pi2; i < points.Count; i++) { p0 = points[i]; if (i < points.Count - 1 && indexNeedsResplitting != 0) { p1 = points[i + 1]; if (EMS.ThinkGeoLibrary.MapSuite.Utilities.MeridianTester.LineSegmentChangesDirection(p0, p1)) indexNeedsResplitting = 0; } westernLine.Vertices.Add(p0); } if (eastLineNeedsResplitting && indexNeedsResplitting == 0) indexNeedsResplitting = 2; return new LineShape[]{westernLine, easternLine }; } } public class TreatedShape { public BaseShape Original { get; set; } public BaseShape Treated { get; set; } /// /// Possible values are: Wrapped, FillToEdge /// public GeometryTreatment Treatment { get; set; } } }