Question

I am searching for a way to calculate the surface under a polygon.

The thing I want to accomplish is that a user that uses my program, can create a polygon to mark out his property. Now I want to know what the surface area is so I can tell the user how big his property is.

Unit m² or km² or hectare.

The points of the polygon have a latitude and longitude.

I am using C# with WPF and GMap.NET. The map is in a WindowsFormHost so I can use the Winforms thing from GMap.Net because this provoides overlays etc.

I hope that someone can help me or show me a post where this is explained that I didn't found.

Was it helpful?

Solution

Using a 2D vector space approximation (local tangent space)

In this section, I can detail how I come to these formulas.

Let's note Points the points of the polygon (where Points[0] == Points[Points.Count - 1] to close the polygon).

The idea behind the next methods is to split the polygon into triangles (the area is the sum of all triangle areas). But, to support all polygon types with a simple decomposition (not only star-shaped polygon), some triangle contributions are negative (we have a "negative" area). The triangles decomposition I use is : {(O, Points[i], Points[i + 1]} where O is the origin of the affine space.

The area of a non-self-intersecting polygon (in euclidian geometry) is given by:

In 2D:

float GetArea(List<Vector2> points)
{
    float area2 = 0;
    for (int numPoint = 0; numPoint < points.Count - 1; numPoint++)
    {
        MyPoint point = points[numPoint];
        MyPoint nextPoint = points[numPoint + 1];
        area2 += point.x * nextPoint.y - point.y * nextPoint.x;
    }
    return area2 / 2f;
}

In 3D, given normal, the unitary normal of the polygon (which is planar):

float GetArea(List<Vector3> points, Vector3 normal)
{
    Vector3 vector = Vector3.Zero;
    for (int numPoint = 0; numPoint < points.Count - 1; numPoint++)
    {
        MyPoint point = points[numPoint];
        MyPoint nextPoint = points[numPoint + 1];
        vector += Vector3.CrossProduct(point, nextPoint);
    }
    return (1f / 2f) * Math.Abs(Vector3.DotProduct(vector, normal));
}

In the previous code I assumed you have a Vector3 struct with Add, Subtract, Multiply, CrossProduct and DotProduct operations.

In your case, you have a lattitude and longitude. Then, you are not in an 2D euclidean space. It is a spheric space where computing the area of any polygon is much more complex. However, it is locally homeomorphic to a 2D vector space (using the tangent space). Then, if the area you try to measure is not too wide (few kilometers), the above formula should work.

Now, you just have to find the normal of the polygon. To do so, and to reduce the error (because we are approximating the area), we use the normal at the centroid of the polygon. The centroid is given by:

Vector3 GetCentroid(List<Vector3> points)
{
    Vector3 vector = Vector3.Zero;
    Vector3 normal = Vector3.CrossProduct(points[0], points[1]);  // Gets the normal of the first triangle (it is used to know if the contribution of the triangle is positive or negative)
    normal = (1f / normal.Length) * normal;  // Makes the vector unitary
    float sumProjectedAreas = 0;
    for (int numPoint = 0; numPoint < points.Count - 1; numPoint++)
    {
        MyPoint point = points[numPoint];
        MyPoint nextPoint = points[numPoint + 1];
        float triangleProjectedArea = Vector3.DotProduct(Vector3.CrossProduct(point, nextPoint), normal);
        sumProjectedAreas += triangleProjectedArea;
        vector += triangleProjectedArea  * (point + nextPoint);
    }
    return (1f / (6f * sumProjectedAreas)) * vector;
}

I've added a new property to Vector3 : Vector3.Length

Finally, to convert latitude and longitude into a Vector3:

Vector3 GeographicCoordinatesToPoint(float latitude, float longitude)
{
    return EarthRadius * new Vector3(Math.Cos(latitude) * Math.Cos(longitude), Math.Cos(latitude) * Math.Sin(longitude), Math.Sin(latitude));
}

To sum up:

// Converts the latitude/longitude coordinates to 3D coordinates
List<Vector3> pointsIn3D = (from point in points
                            select GeographicCoordinatesToPoint(point.Latitude, point.Longitude))
                           .ToList();

// Gets the centroid (to have the normal of the vector space)
Vector3 centroid = GetCentroid(pointsIn3D );

// As we are on a sphere, the normal at a given point is the colinear to the vector going from the center of the sphere to the point.
Vector3 normal = (1f / centroid.Length) * centroid;  // We want a unitary normal.

// Finally the area is computed using:
float area = GetArea(pointsIn3D, normal);

The Vector3 struct

public struct Vector3
{
    public static readonly Vector3 Zero = new Vector3(0, 0, 0);

    public readonly float X;
    public readonly float Y;
    public readonly float Z;

    public float Length { return Math.Sqrt(X * X + Y * Y + Z * Z); }

    public Vector3(float x, float y, float z)
    {
         X = x;
         Y = y;
         Z = z;
    }

    public static Vector3 operator +(Vector3 vector1, Vector3 vector2)
    {
        return new Vector3(vector1.X + vector2.X, vector1.Y + vector2.Y, vector1.Z + vector2.Z);
    }
    public static Vector3 operator -(Vector3 vector1, Vector3 vector2)
    {
        return new Vector3(vector1.X - vector2.X, vector1.Y - vector2.Y, vector1.Z - vector2.Z);
    }
    public static Vector3 operator *(float scalar, Vector3 vector)
    {
        return new Vector3(scalar * vector.X, scalar * vector.Y, scalar * vector.Z);
    }

    public static float DotProduct(Vector3 vector1, Vector3 vector2)
    {
        return vector1.X * vector2.X + vector1.Y * vector2.Y + vector1.Z * vector2.Z;
    }
    public static Vector3 CrossProduct(Vector3 vector1, Vector3 vector2)
    {
        return return new Vector3(vector1.Y * vector2.Z - vector1.Z * vector2.Y,
                                  vector1.Z * vector2.X - vector1.X * vector2.Z,
                                  vector1.X * vector2.Y - vector1.Y * vector2.X);
    }
}

OTHER TIPS

I fixed it with part of the code from Cédric and code from the internet.

I fixed it by using poly.Points and poly.LocalPoints. The poly.Points are the latitude and longitude while the LocalPoints are points see to the center of the map on the screen.

the C# library has a function to calculate the distance (km) so I calculted the distance and then I calculated the distance in LocalPoints. Dived the localPoints throug the length in km and then you know how long 1 Localpoint is in km.

Code:

List<PointLatLng> firstTwoPoints = new List<PointLatLng>();
         firstTwoPoints.Add(poly.Points[0]);
         firstTwoPoints.Add(poly.Points[1]);

         GMapPolygon oneLine = new GMapPolygon(firstTwoPoints,"testesddfsdsd"); //Create new polygone from messuring the distance.
         double lengteLocalLine =
            Math.Sqrt(((poly.LocalPoints[1].X - poly.LocalPoints[0].X)*(poly.LocalPoints[1].X - poly.LocalPoints[0].X)) +
                      ((poly.LocalPoints[1].Y - poly.LocalPoints[0].Y)*(poly.LocalPoints[1].Y - poly.LocalPoints[0].Y))); //This calculates the length of the line in LocalPoints. 
         double pointInKm = oneLine.Distance / lengteLocalLine; //This gives me the length of 1 LocalPoint in km.

         List<Carthesian> waarden = new List<Carthesian>(); 

//Here we fill the list "waarden" with the points.
//NOTE: the last value is NOT copied because this is handled in calculation method.
         foreach (GPoint localPoint in poly.LocalPoints)
         {
            waarden.Add(new Carthesian(){X = (localPoint.X * pointInKm), Y = (localPoint.Y * pointInKm)});
         }

         MessageBox.Show("" + GetArea(waarden)*1000000);

    }

//Method for calculating area
      private double GetArea(IList<Carthesian> points)
      {
         if (points.Count < 3)
         {
            return 0;
         }
         double area = GetDeterminant(points[points.Count - 1].X , points[points.Count - 1].Y, points[0].X, points[0].Y);

         for (int i = 1; i < points.Count; i++)
         {
            //Debug.WriteLine("Lng: " + points[i].Lng + "   Lat:" + points[i].Lat);
            area += GetDeterminant(points[i - 1].X, points[i - 1].Y , points[i].X,  points[i].Y);
         }
         return Math.Abs(area / 2);
      }

//Methode for getting the Determinant

      private double GetDeterminant(double x1, double y1, double x2, double y2)
      {
         return x1 * y2 - x2 * y1;
      }



//This class is just to make it nicer to show in code and it also was from previous tries
    class Carthesian
       {
          public double X { get; set; }
          public double Y { get; set; }
          public double Z { get; set; }
       }

Because we calculate the surface using 2D there is a small error, but for my application this is acceptable.

And thanks to Cédric for answering my question and helping me to fix the problem I had.

Its much easier to just use a backend database like SQL Server 2008 or MySql, sending the points of the polygon to the server in a query and return area, length, parimeter, intersection...etc.

If this is viable, search STArea() or STIntersect on Sql Server geography/geometry datatypes.

here is an example of something I have been working on.

using Microsoft.SqlServer.Types;
using System.Data.SqlClient;

    GMap.NET.WindowsForms.GMapOverlay o = new GMapOverlay();
    GMap.NET.WindowsForms.GMapOverlay o1 = new GMapOverlay();
    List<PointLatLng> p = new List<PointLatLng>();
    List<string> p1 = new List<string>();

//below adds a marker to the map upon each users click. At the same time adding that Lat/Long to a <PointLatLng> list
    private void gMapControl1_MouseClick(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Right )
        {
            p.Add(new PointLatLng(Convert.ToDouble(gMapControl2.FromLocalToLatLng(e.X, e.Y).Lat), Convert.ToDouble(gMapControl2.FromLocalToLatLng(e.X, e.Y).Lng)));
            p1.Add(gMapControl2.FromLocalToLatLng(e.X, e.Y).Lng + " " + gMapControl2.FromLocalToLatLng(e.X, e.Y).Lat);
            GMarkerGoogle marker = new GMarkerGoogle(gMapControl2.FromLocalToLatLng(e.X, e.Y), GMarkerGoogleType.red_small);
           // marker.Tag =(gMapControl1.FromLocalToLatLng(e.X, e.Y).Lng + " " + gMapControl1.FromLocalToLatLng(e.X, e.Y).Lat);

            o.Markers.Add(marker);
            gMapControl2.Overlays.Add(o);
        }
    }

//Then the below code will add that <PointLatLng> List to a SQL query and return Area and Centoid of polygon. Area is returned in Acres
 private void gMapControl1_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            try
            {
                o.Clear();

                n = new GMapPolygon(p, "polygon");
                n.Fill = new SolidBrush(Color.Transparent);

                n.Stroke = new Pen(Color.Red, 1);
                o.Polygons.Add(n);

                gMapControl2.Overlays.Add(o);
                StringBuilder a = new StringBuilder();
                StringBuilder b = new StringBuilder();
                p1.ToArray();

                for (int i = 0; i != p1.Count; i++)
                {
                    a.Append(p1[i].ToString() + ",");
                }
                a.Append(p1[0].ToString());

                cs.Open();
                SqlCommand cmd = new SqlCommand("Declare @g geography; set @g = 'Polygon((" + a + "))'; Select Round((@g.STArea() *0.00024711),3) As Area", cs);
                SqlCommand cmd1 = new SqlCommand("Declare @c geometry; set @c =geometry::STGeomFromText('Polygon((" + a + "))',0);  Select Replace(Replace(@c.STCentroid().ToString(),'POINT (',''),')','')AS Center", cs);

                poly = "Polygon((" + a + "))";

                SqlDataReader sdr = cmd.ExecuteReader();

                while (sdr.Read())
                {
                    txtArea.Text = sdr["Area"].ToString();
                }
                sdr.Close();

                SqlDataReader sdr1 = cmd1.ExecuteReader();
                while (sdr1.Read())
                {
                    center = sdr1["Center"].ToString();
                    lat = center.Substring(center.IndexOf(" ") + 1);
                    lat = lat.Remove(9);
                    lon = center.Substring(0, (center.IndexOf(" ")));
                    lon = lon.Remove(10);
                    txtCenter.Text = lat + ", " + lon;
                }

                sdr1.Close();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Please start the polygon over, you must create polygon in a counter-clockwise fasion","Counter-Clockwise Only!",MessageBoxButtons.OK,MessageBoxIcon.Error);
            }
            finally
            {
                p.Clear();
                p1.Clear();
                cs.Close();
                o.Markers.Clear();
            }
        }
    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top