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