Merging polygons on curved surface

294 Views Asked by At

I tried the clipper library for merging polygons (c#) that I want to import into Google Earth. It worked with a flat-surface sample of two circles, but there are two problems with using longitute and latitude as X and Y:

  1. Clipper only supports long as datatype (you can get around that be using a multiplier before passing the values to Clipper - there is already a question about that, but it doesn't address the second point
  2. Clipper assumes the surface is flat, so the output coordinates are not where they are expected. It's not possible to just think of X and Y as longitude and latitude and add value x to them. It may work at the equator, but higher up north adding the same value to longitude would result in a shorter distance.

enter image description here

I was wondering if there are already implementations / libraries for that scenario.

Update: As the source code that was used has been requested, I created a small sample application that contains the relevant code:

internal class Polygon : List<DoublePoint> { }
internal class Polygons : List<List<DoublePoint>> { }

internal class IntPolygon : List<IntPoint> { }
internal class IntPolygons : List<List<IntPoint>> { }

class Program
{
    static void Main(string[] args)
    {
        MercatorProjection mercatorProjection = new MercatorProjection();

        List<GeoCoordinate> circle1Coordinates = GenerateCirclePolygonByMeters(new GeoCoordinate { Longitude = 50, Latitude = 50 }, 5000);
        List<GeoCoordinate> circle2Coordinates = GenerateCirclePolygonByMeters(new GeoCoordinate { Longitude = 50.05, Latitude = 50 }, 5000);

        Polygons polygons = new Polygons();

        Polygon circle1 = new Polygon();
        foreach(var coordinate in circle1Coordinates)
        {
            DoublePoint point = mercatorProjection.FromLatLngToPoint(coordinate.Latitude, coordinate.Longitude, 15);
            circle1.Add(point);
        }

        Polygon circle2 = new Polygon();
        foreach (var coordinate in circle2Coordinates)
        {
            DoublePoint point = mercatorProjection.FromLatLngToPoint(coordinate.Latitude, coordinate.Longitude, 15);
            circle2.Add(point);
        }

        polygons.Add(circle1);
        polygons.Add(circle2);

        // Workaround: get int-values
        IntPolygons intPolygons = ConvertToIntPolygons(polygons);

        // Merge
        IntPolygons mergedIntPolygons = MergePolygons(intPolygons);

        // Workaroud: Get doubles again
        Polygons mergedPolygons = ConvertToDoublePolygons(mergedIntPolygons);

        // Convert back to spherical surface
        // GeoCoordinate class is from System.Device
        List<GeoCoordinate> mergedCoordinates = new List<GeoCoordinate>();
        foreach (var polygon in mergedPolygons)
        {
            foreach (var point in polygon)
            {
                GeoCoordinate coordinate = mercatorProjection.FromPointToLatLng(point, 15);
                mergedCoordinates.Add(coordinate);
            }
        }

        // Generate output csv-list
        WriteOutputFile(mergedCoordinates);
    }

    private static void WriteOutputFile(List<GeoCoordinate> coordinates, string filename = "")
    {
        string uniquename = DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".txt";
        string filenameToUse = String.IsNullOrEmpty(filename) ? uniquename : filename;
        var fileStream = File.OpenWrite(filenameToUse);

        using (var writer = new StreamWriter(fileStream))
        {
            // header
            writer.WriteLine("lat,long");

            // content
            foreach (var coordinate in coordinates)
            {
                writer.WriteLine(coordinate.Latitude.ToString(CultureInfo.InvariantCulture) + "," + coordinate.Longitude.ToString(CultureInfo.InvariantCulture));
            }

            writer.Close();
        }
    }

    private static Polygons ConvertToDoublePolygons(IntPolygons polygons)
    {
        double multiplier = 100;

        Polygons doublePolygons = new Polygons();
        foreach (var intPolygon in polygons)
        {
            Polygon doublePolygon = new Polygon();
            foreach (var intPoint in intPolygon)
            {
                doublePolygon.Add(new DoublePoint { X = intPoint.X / multiplier, Y = intPoint.Y / multiplier });
            }

            doublePolygons.Add(doublePolygon);
        }

        return doublePolygons;
    }

    private static IntPolygons ConvertToIntPolygons(Polygons polygons)
    {
        double multiplier = 100;
        IntPolygons intPolygons = new IntPolygons();
        foreach (var doublePolygon in polygons)
        {
            IntPolygon intPolygon = new IntPolygon();
            foreach (var doublePoint in doublePolygon)
            {
                intPolygon.Add(new IntPoint { X = (int)(doublePoint.X * multiplier), Y = (int)(doublePoint.Y * multiplier) });
            }

            intPolygons.Add(intPolygon);
        }

        return intPolygons;
    }

    private static List<GeoCoordinate> GenerateCirclePolygonByMeters(GeoCoordinate position, double distanceMeters)
    {
        // calc distance in coordinates-system
        double latitude2 = CalculateDerivedPosition(position, distanceMeters, 0).Latitude;
        double yRadius = latitude2 - position.Latitude;
        double longitude2 = CalculateDerivedPosition(position, distanceMeters, 90).Longitude;
        double xRadius = longitude2 - position.Longitude;

        var circle = GenerateCirclePolygon(position.Longitude, position.Latitude, xRadius, yRadius, 16);

        List<GeoCoordinate> circleCoordinates = new List<GeoCoordinate>();
        foreach(var point in circle)
        {
            circleCoordinates.Add(new GeoCoordinate { Latitude = point.Y, Longitude = point.X });
        }

        return circleCoordinates;
    }

    private static Polygon GenerateCirclePolygon(double xStart, double yStart, double xRadius, double yRadius, int points)
    {
        Polygon polygon = new Polygon();
        double slice = 2 * Math.PI / points;


        for (int i = 0; i < points; i++)
        {
            double angle = slice * i;
            Console.WriteLine(angle);
            double x = xRadius * Math.Cos(angle);
            double y = yRadius * Math.Sin(angle);

            polygon.Add(new DoublePoint(xStart + x, yStart + y));
        }

        return polygon;
    }

    // Source: https://stackoverflow.com/questions/1125144/how-do-i-find-the-lat-long-that-is-x-km-north-of-a-given-lat-long
    public static GeoCoordinate CalculateDerivedPosition(GeoCoordinate source, double rangeMeters, double bearing)
    {
        double radiansToDegrees = 57.2957795;
        double degreesToRadians = 0.0174532925;
        double twoPi = Math.PI * 2;
        double earthRadius = 6378137.0;

        double latA = source.Latitude * degreesToRadians;
        double lonA = source.Longitude * degreesToRadians;
        double angularDistance = rangeMeters / earthRadius;
        double trueCourse = bearing * degreesToRadians;

        double lat = Math.Asin(
            Math.Sin(latA) * Math.Cos(angularDistance) +
            Math.Cos(latA) * Math.Sin(angularDistance) * Math.Cos(trueCourse));

        double dlon = Math.Atan2(
            Math.Sin(trueCourse) * Math.Sin(angularDistance) * Math.Cos(latA),
            Math.Cos(angularDistance) - Math.Sin(latA) * Math.Sin(lat));

        double lon = ((lonA + dlon + Math.PI) % twoPi) - Math.PI;

        return new GeoCoordinate(
            lat * radiansToDegrees,
            lon * radiansToDegrees);
    }

    private static IntPolygons MergePolygons(IntPolygons polygons)
    {
        Clipper clipper = new Clipper();

        clipper.AddPaths(polygons, PolyType.ptSubject, true);

        IntPolygons mergedPolygons = new IntPolygons();
        clipper.Execute(ClipType.ctUnion, mergedPolygons,
            PolyFillType.pftNonZero, PolyFillType.pftNonZero);

        return mergedPolygons;
    }

Update 2: There was actually an error in the way that the circles were created. With the correction the circles get created and merged correctly: enter image description here

I found out that I can use the Mercator Projection to get the points of the circles in the image above projected to a flat surface. Then merging could be done using Clipper. Finally all points were projected to the sphere again (the reverse of the Mercator Projection).

This would be a workaround (will be testing it in more depth). So the question is kind of answered, but it would be interesting if there other solutions with less workarounds.

0

There are 0 best solutions below