Related
I'm trying to get exact pixel count between two points on jpeg image. To do so i use user drawn line in WPF application using Line class. My solution is to calculate distance between two end points of this line seems to be pretty off and it returns fractional number which obviously not pixel count. Here is the code for drawing line:
// The "size" of an object for mouse over purposes.
private const int ObjectRadius = 3;
// We're over an object if the distance squared
// between the mouse and the object is less than this.
private const int OverDistSquared = ObjectRadius * ObjectRadius;
// The line we're drawing or moving.
private Line _selectedLine;
private List<Line> _lines = new List<Line>();
// True if we're moving the line's first starting end point.
private bool _movingStartEndPoint = false;
// The offset from the mouse to the object being moved.
private double _offsetX, _offsetY;
// Save the trash can dimensions.
private double _trashWidth, _trashHeight;
// The mouse is up. See whether we're over an end point or segment.
private void canDrawing_MouseMove_NotDown(object sender, MouseEventArgs e)
{
Cursor newCursor = Cursors.Cross;
// See what we're over.
Point location = e.MouseDevice.GetPosition(canDrawing);
if (MouseIsOverEndpoint(location, out _selectedLine, out _movingStartEndPoint))
newCursor = Cursors.Arrow;
else if (MouseIsOverLine(location, out _selectedLine))
newCursor = Cursors.Hand;
// Set the new cursor.
if (canDrawing.Cursor != newCursor)
canDrawing.Cursor = newCursor;
}
// See what we're over and start doing whatever is appropriate.
private void canDrawing_MouseDown(object sender, MouseButtonEventArgs e)
{
// See what we're over.
Point location = e.MouseDevice.GetPosition(canDrawing);
if (MouseIsOverEndpoint(location, out _selectedLine, out _movingStartEndPoint))
{
// Start moving this end point.
canDrawing.MouseMove -= canDrawing_MouseMove_NotDown;
canDrawing.MouseMove += canDrawing_MouseMove_MovingEndPoint;
canDrawing.MouseUp += canDrawing_MouseUp_MovingEndPoint;
// Remember the offset from the mouse to the point.
Point hitPoint;
if (_movingStartEndPoint)
hitPoint = new Point(_selectedLine.X1, _selectedLine.Y1);
else
hitPoint = new Point(_selectedLine.X2, _selectedLine.Y2);
_offsetX = hitPoint.X - location.X;
_offsetY = hitPoint.Y - location.Y;
}
else if (MouseIsOverLine(location, out _selectedLine))
{
// Start moving this segment.
canDrawing.MouseMove -= canDrawing_MouseMove_NotDown;
canDrawing.MouseMove += canDrawing_MouseMove_MovingSegment;
canDrawing.MouseUp += canDrawing_MouseUp_MovingSegment;
// Remember the offset from the mouse
// to the segment's first end point.
_offsetX = _selectedLine.X1 - location.X;
_offsetY = _selectedLine.Y1 - location.Y;
}
else
{
// Start drawing a new segment.
canDrawing.MouseMove -= canDrawing_MouseMove_NotDown;
canDrawing.MouseMove += canDrawing_MouseMove_Drawing;
canDrawing.MouseUp += canDrawing_MouseUp_Drawing;
_selectedLine = new Line
{
Stroke = Brushes.Red,
X1 = location.X,
Y1 = location.Y,
X2 = location.X,
Y2 = location.Y
};
canDrawing.Children.Add(_selectedLine);
}
}
#region Distance Methods
// See if the mouse is over an end point.
private bool MouseIsOverEndpoint(Point mousePt, out Line hitLine, out bool startEndpoint)
{
foreach (object obj in canDrawing.Children)
{
// Only process Lines.
if (obj is Line)
{
Line line = obj as Line;
// Check the starting point.
Point point = new Point(line.X1, line.Y1);
if (FindDistanceToPointSquared(mousePt, point) < OverDistSquared)
{
// We're over this point.
hitLine = line;
startEndpoint = true;
return true;
}
// Check the end point.
point = new Point(line.X2, line.Y2);
if (FindDistanceToPointSquared(mousePt, point) < OverDistSquared)
{
// We're over this point.
hitLine = line;
startEndpoint = false;
return true;
}
}
}
hitLine = null;
startEndpoint = false;
return false;
}
// See if the mouse is over a line segment.
private bool MouseIsOverLine(Point mousePt, out Line hitLine)
{
foreach (object obj in canDrawing.Children)
{
// Only process Lines.
if (obj is Line)
{
Line line = obj as Line;
// See if we're over this line.
Point closest;
Point pt1 = new Point(line.X1, line.Y1);
Point pt2 = new Point(line.X2, line.Y2);
if (FindDistanceToSegmentSquared(
mousePt, pt1, pt2, out closest)
< OverDistSquared)
{
// We're over this segment.
hitLine = line;
return true;
}
}
}
hitLine = null;
return false;
}
// Calculate the distance squared between two points.
private double FindDistanceToPointSquared(Point pt1, Point pt2)
{
double dx = pt1.X - pt2.X;
double dy = pt1.Y - pt2.Y;
return dx * dx + dy * dy;
}
// Calculate the distance squared between
// point pt and the segment p1 --> p2.
private double FindDistanceToSegmentSquared(Point pt, Point p1, Point p2, out Point closest)
{
double dx = p2.X - p1.X;
double dy = p2.Y - p1.Y;
if ((dx == 0) && (dy == 0))
{
// It's a point not a line segment.
closest = p1;
dx = pt.X - p1.X;
dy = pt.Y - p1.Y;
return dx * dx + dy * dy;
}
// Calculate the t that minimizes the distance.
double t = ((pt.X - p1.X) * dx + (pt.Y - p1.Y) * dy) / (dx * dx + dy * dy);
// See if this represents one of the segment's
// end points or a point in the middle.
if (t < 0)
{
closest = new Point(p1.X, p1.Y);
dx = pt.X - p1.X;
dy = pt.Y - p1.Y;
}
else if (t > 1)
{
closest = new Point(p2.X, p2.Y);
dx = pt.X - p2.X;
dy = pt.Y - p2.Y;
}
else
{
closest = new Point(p1.X + t * dx, p1.Y + t * dy);
dx = pt.X - closest.X;
dy = pt.Y - closest.Y;
}
return dx * dx + dy * dy;
}
private double FindDistanceToPoint(Point pt1, Point pt2)
{
double dx = pt1.X - pt2.X;
double dy = pt1.Y - pt2.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
#endregion Distance Methods
#region Moving End Point
// We're moving an end point.
private void canDrawing_MouseMove_MovingEndPoint(object sender, MouseEventArgs e)
{
// Move the point to its new location.
Point location = e.MouseDevice.GetPosition(canDrawing);
if (_movingStartEndPoint)
{
_selectedLine.X1 = location.X + _offsetX;
_selectedLine.Y1 = location.Y + _offsetY;
}
else
{
_selectedLine.X2 = location.X + _offsetX;
_selectedLine.Y2 = location.Y + _offsetY;
}
}
// Stop moving the end point.
private void canDrawing_MouseUp_MovingEndPoint(object sender, MouseEventArgs e)
{
// Reset the event handlers.
canDrawing.MouseMove += canDrawing_MouseMove_NotDown;
canDrawing.MouseMove -= canDrawing_MouseMove_MovingEndPoint;
canDrawing.MouseUp -= canDrawing_MouseUp_MovingEndPoint;
}
#endregion Moving End Point
#region Drawing
// We're drawing a new segment.
private void canDrawing_MouseMove_Drawing(object sender, MouseEventArgs e)
{
// Update the new line's end point.
Point location = e.MouseDevice.GetPosition(canDrawing);
_selectedLine.X2 = location.X;
_selectedLine.Y2 = location.Y;
}
// Stop drawing.
private void canDrawing_MouseUp_Drawing(object sender, MouseEventArgs e)
{
_selectedLine.Stroke = Brushes.DeepPink;
// Reset the event handlers.
canDrawing.MouseMove -= canDrawing_MouseMove_Drawing;
canDrawing.MouseMove += canDrawing_MouseMove_NotDown;
canDrawing.MouseUp -= canDrawing_MouseUp_Drawing;
// If the new segment has no length, delete it.
if ((_selectedLine.X1 == _selectedLine.X2) && (_selectedLine.Y1 == _selectedLine.Y2))
canDrawing.Children.Remove(_selectedLine);
else
{
_lines.Add(_selectedLine);
var point1 = new Point(_selectedLine.X1, _selectedLine.Y1);
var point2 = new Point(_selectedLine.X2, _selectedLine.Y2);
PixelsInMillimeterTextBox.Text = FindDistanceToPoint(point1, point2).ToString(CultureInfo.InvariantCulture);
}
}
#endregion Drawing
#region "Moving Segment"
// We're moving a segment.
private void canDrawing_MouseMove_MovingSegment(object sender, MouseEventArgs e)
{
// Find the new location for the first end point.
Point location = e.MouseDevice.GetPosition(canDrawing);
double newX1 = location.X + _offsetX;
double newY1 = location.Y + _offsetY;
// See how far we are moving that point.
double dx = newX1 - _selectedLine.X1;
double dy = newY1 - _selectedLine.Y1;
// Move the line.
_selectedLine.X1 = newX1;
_selectedLine.Y1 = newY1;
_selectedLine.X2 += dx;
_selectedLine.Y2 += dy;
}
// Stop moving the segment.
private void canDrawing_MouseUp_MovingSegment(object sender, MouseEventArgs e)
{
// Reset the event handlers.
canDrawing.MouseMove += canDrawing_MouseMove_NotDown;
canDrawing.MouseMove -= canDrawing_MouseMove_MovingSegment;
canDrawing.MouseUp -= canDrawing_MouseUp_MovingSegment;
// See if the mouse is over the trash can.
Point location = e.MouseDevice.GetPosition(canDrawing);
if ((location.X >= 0) && (location.X < _trashWidth) &&
(location.Y >= 0) && (location.Y < _trashHeight))
{
if (MessageBox.Show("Delete this segment?",
"Delete Segment?", MessageBoxButton.YesNo)
== MessageBoxResult.Yes)
{
// Delete the segment.
canDrawing.Children.Remove(_selectedLine);
}
}
}
#endregion // Moving End Point
And here is the XAML for the control containing image:
<Grid>
<Border Name="Border" BorderThickness="1" BorderBrush="#34558b" Margin="10,44,10,10">
<utility:ZoomBorder x:Name="border" ClipToBounds="True" Background="Gray">
<Canvas Name="canDrawing"
MouseMove="canDrawing_MouseMove_NotDown"
MouseDown="canDrawing_MouseDown">
<Image Stretch="None" Name="ReferenceImage" Canvas.Left="0" Canvas.Top="0"/>
</Canvas>
</utility:ZoomBorder>
</Border>
</Grid>
There is a Win32 GDI call (LineDDA) that will enumerate the points (pixels) between two points.
Getting the count of points will give you the length of the line in pixels.
This answer has C# code that you can use. I tried it with your sample code and was able to get a proper pixel length.
Here is the code for reference:
public static List<Point> GetPointsOnLine(System.Drawing.Point point1, System.Drawing.Point point2)
{
var points = new List<Point>();
var handle = GCHandle.Alloc(points);
try
{
LineDDA(point1.X, point1.Y, point2.X, point2.Y, GetPointsOnLineCallback, GCHandle.ToIntPtr(handle));
}
finally
{
handle.Free();
}
return points;
}
private static void GetPointsOnLineCallback(int x, int y, IntPtr lpData)
{
var handle = GCHandle.FromIntPtr(lpData);
var points = (List<Point>)handle.Target;
points.Add(new Point(x, y));
}
[DllImport("gdi32.dll")]
private static extern bool LineDDA(int nXStart, int nYStart, int nXEnd, int nYEnd, LineDDAProc lpLineFunc, IntPtr lpData);
// The signature for the callback method
private delegate void LineDDAProc(int x, int y, IntPtr lpData);
Note, that the GetPointsOnLine method using System.Drawing.Point instead of System.Windows.Point.
I'm trying to rotate polygon randomly using timer. I got to draw regular polygons and rotate it to one direction. But I'm not sure about how to rotate polygon to random direction using angle or timer interval.
My Code is below:
int sides = 5;
Graphics g = e.Graphics;
nPoints = CalculateVertices(sides, radius, angle, center);
g.DrawPolygon(navypen, nPoints);
g.FillPolygon(BlueBrush, nPoints);
Point center = new Point(ClientSize.Width / 2, ClientSize.Height / 2);
for(int i = 0; i < sides; i++) {
g.DrawLine(new Pen(Color.Navy), center.X, center.Y, nPoints[i].X, nPoints[i].Y);
}
private PointF[] CalculateVertices(int sides, int radius, float startingAngle, Point center)
{
if (sides < 3) {
sides = 3;
}
List<PointF> points = new List<PointF>();
float step = 360.0f / sides;
float angle = startingAngle; //starting angle
for (double i = startingAngle; i < startingAngle + 360.0; i += step) //go in a circle
{
points.Add(DegreesToXY(angle, radius, center));
angle += step;
}
return points.ToArray();
}
private PointF DegreesToXY(float degrees, float radius, Point origin)
{
PointF xy = new PointF();
double radians = degrees * Math.PI / 180.0;
xy.X = (int)(Math.Cos(radians) * radius + origin.X);
xy.Y = (int)(Math.Sin(-radians) * radius + origin.Y);
return xy;
}
private void timer2_Tick(object sender, EventArgs e){
angle += 1;
angle_tri -= 1;
Invalidate();
}
Here is an example of drawing a list of points rotated with varying speeds, both angular and and timing..:
First a few variables:
Random rnd = new Random();
float angle = 0f;
List<Point> points = new List<Point>();
Then a Tick with varying speed and a varying angle:
private void timer1_Tick(object sender, EventArgs e)
{
angle += rnd.Next(0, 33)/ 10f;
timer1.Interval = rnd.Next(100) + 15;
pictureBox5.Invalidate();
}
Here is the Paint event of a PictureBox, which is DoubleBuffered, so it won't flicker..:
private void pictureBox5_Paint(object sender, PaintEventArgs e)
{
if (points.Count > 1)
{
Point center = new Point(
(points.Select(x => x.X).Max() + points.Select(x => x.X).Min()) / 2,
(points.Select(x => x.Y).Max() + points.Select(x => x.Y).Min()) / 2);
e.Graphics.TranslateTransform(center.X, center.Y);
e.Graphics.RotateTransform(angle);
e.Graphics.TranslateTransform(-center.X, -center.Y);
e.Graphics.DrawPolygon(Pens.DarkGreen, points.ToArray());
}
}
Note that due to the weird speed changes this is anything but smooth; you would have to find better algorithms for them than mere randomness..
I've been tried to draw triangles on each edge of regular polygons.
So far I got to make polygons like this:
What I'm trying to make is that small triangle on the each edge of the polygon:
How do I do this?
Code how to draw polygons is below:
int sides = 5;
Graphics g = e.Graphics;
nPoints = CalculateVertices(sides, radius, angle, center);
g.DrawPolygon(navypen, nPoints);
g.FillPolygon(BlueBrush, nPoints);
Point center = new Point(ClientSize.Width / 2, ClientSize.Height / 2);
for(int i = 0; i < sides; i++) {
g.DrawLine(new Pen(Color.Navy), center.X, center.Y, nPoints[i].X, nPoints[i].Y);
}
private PointF[] CalculateVertices(int sides, int radius, float startingAngle, Point center)
{
if (sides < 3) {
sides = 3;
}
//throw new ArgumentException("Polygon must have 3 sides or more.");
List<PointF> points = new List<PointF>();
float step = 360.0f / sides;
float angle = startingAngle; //starting angle
for (double i = startingAngle; i < startingAngle + 360.0; i += step) //go in a circle
{
points.Add(DegreesToXY(angle, radius, center));
angle += step;
}
return points.ToArray();
}
private PointF DegreesToXY(float degrees, float radius, Point origin)
{
PointF xy = new PointF();
double radians = degrees * Math.PI / 180.0;
xy.X = (int)(Math.Cos(radians) * radius + origin.X);
xy.Y = (int)(Math.Sin(-radians) * radius + origin.Y);
return xy;
}
Try this out...instead of calculating absolute points for the triangle, I've instead computed points for a "unit triangle" at the origin (using your function!). Then I simply rotate and move the Graphics surface and draw the unit triangle where I want it:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private PointF[] nPoints;
private PointF[] triangle;
private int sides = 5;
private int angle = 0;
private int radius = 100;
private int triangleLength = 10;
private void Form1_Load(object sender, EventArgs e)
{
triangle = this.CalculateVertices(3, triangleLength, 0, new Point(0, 0)); // this "unit triangle" will get reused!
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Point center = new Point(ClientSize.Width / 2, ClientSize.Height / 2);
nPoints = CalculateVertices(sides, radius, angle, center);
// draw the polygon
g.FillPolygon(Brushes.Blue, nPoints);
g.DrawPolygon(Pens.Black, nPoints);
for (int i = 0; i < sides; i++)
{
g.DrawLine(Pens.Black, center.X, center.Y, nPoints[i].X, nPoints[i].Y);
}
// draw small triangles on each edge:
float step = 360.0f / sides;
float curAngle = angle + step / 2; // start in-between the original angles
for (double i = curAngle; i < angle + (step / 2) + 360.0; i += step) //go in a circle
{
// move to the center and rotate:
g.ResetTransform();
g.TranslateTransform(center.X, center.Y);
g.RotateTransform((float)i);
// move out to where the triangle will be drawn and render it
g.TranslateTransform(radius, 0);
g.FillPolygon(Brushes.LightGreen, triangle);
g.DrawPolygon(Pens.Black, triangle);
}
}
// this is your code unchanged
private PointF[] CalculateVertices(int sides, int radius, float startingAngle, Point center)
{
if (sides < 3)
{
sides = 3;
}
//throw new ArgumentException("Polygon must have 3 sides or more.");
List<PointF> points = new List<PointF>();
float step = 360.0f / sides;
float angle = startingAngle; //starting angle
for (double i = startingAngle; i < startingAngle + 360.0; i += step) //go in a circle
{
points.Add(DegreesToXY(angle, radius, center));
angle += step;
}
return points.ToArray();
}
// this is your code unchanged
private PointF DegreesToXY(float degrees, float radius, Point origin)
{
PointF xy = new PointF();
double radians = degrees * Math.PI / 180.0;
xy.X = (int)(Math.Cos(radians) * radius + origin.X);
xy.Y = (int)(Math.Sin(-radians) * radius + origin.Y);
return xy;
}
}
I'm able to point zoom on the Mandelbrot set, as long as the mouse doesn't move after zooming has begun. I've tried calculating a normalized delta (new coordinate - old coordinate)*(oldzoom), but what happens is the image appears to jump around to a new location. I've seen this issue before. I'm struggling more here because I have to somehow convert this mouse position delta back to the -2,2 coordinate space of the Mandelbrot set.
Here's my code. What's important is the GetZoomPoint method, and then the lines of code that define x0 and y0. Also, I use the Range class to scale values from one range to another. I WAS using deltaTrans (thats the thing I was talking about earlier where I normalize the mouse delta with the old scale).
using OpenTK.Graphics.OpenGL;
using SpriteSheetMaker;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Fractal.Fractal
{
public class Mandelbrot : BaseTexture
{
private static Transform GlobalTransform = SpriteSheetMaker.Global.Transform;
private static Vector3 GlobalScale = GlobalTransform.Scale;
private static Vector3 GlobalTrans = GlobalTransform.Translation;
private static Vector3 LastWindowPoint = null;
private static Vector3 ZoomFactor = Vector3.ONE * 1.2f;
private static Vector3 Displacement = Vector3.ZERO;
private static int WindowSize = 100;
public static Vector3 GetZoomPoint()
{
var zP = OpenGLHelpers.LastZoomPoint.Clone();
if (LastWindowPoint == null)
{
LastWindowPoint = zP.Clone();
}
var delta = zP - LastWindowPoint;
var oldZoom = GlobalScale / ZoomFactor;
var deltaTrans = delta.XY * oldZoom.XY;
var factor = ZoomFactor.Clone();
Range xR = new Range(0, WindowSize);
Range yR = new Range(0, WindowSize);
Range complexRange = new Range(-2, 2);
// Calculate displacement of zooming position.
var dx = (zP.X - Displacement.X) * (factor.X - 1f);
var dy = (zP.Y - Displacement.Y) * (factor.Y - 1f);
// Compensate for displacement.
Displacement.X -= dx;
Displacement.Y -= dy;
zP -= Displacement;
var x = complexRange.ScaleValue(zP.X, xR);
var y = complexRange.ScaleValue(zP.Y, yR);
var rtn = new Vector3(x, y);
LastWindowPoint = zP.Clone();
return rtn;
}
public static Mandelbrot Generate()
{
var size = new Size(WindowSize, WindowSize);
var radius = new Size(size.Width / 2, size.Height / 2);
Bitmap bmp = new Bitmap(size.Width, size.Height);
LockBitmap.LockBitmapUnsafe lbm = new LockBitmap.LockBitmapUnsafe(bmp);
lbm.LockBits();
var pt = Mandelbrot.GetZoomPoint();
Parallel.For(0, size.Width, i =>
{
// float x0 = complexRangeX.ScaleValue(i, xRange);
float x0 = ((i - radius.Width) / GlobalScale.X) + pt.X;
Parallel.For(0, size.Height, j =>
{
// float y0 = complexRangeY.ScaleValue(j, yRange);
float y0 = ((j - radius.Height) / GlobalScale.Y) + pt.Y;
float value = 0f;
float x = 0.0f;
float y = 0.0f;
int iteration = 0;
int max_iteration = 100;
while (x * x + y * y <= 4.0 && iteration < max_iteration)
{
float xtemp = x * x - y * y + x0;
y = 2.0f * x * y + y0;
x = xtemp;
iteration += 1;
if (iteration == max_iteration)
{
value = 255;
break;
}
else
{
value = iteration * 50f % 255f;
}
}
int v = (int)value;
lbm.SetPixel(i, j, new ColorLibrary.HSL(v / 255f, 1.0, 0.5).ToDotNetColor());
});
});
lbm.UnlockBits();
var tex = new BaseTextureImage(bmp);
var rtn = new Mandelbrot(tex);
return rtn;
}
public override void Draw()
{
base._draw();
}
private Mandelbrot(BaseTextureImage graphic)
{
var topLeft = new Vector3(0, 1);
var bottomLeft = new Vector3(0, 0);
var bottomRight = new Vector3(1, 0);
var topRight = new Vector3(1, 1);
this.Vertices = new List<Vector3>()
{
topLeft,bottomLeft,bottomRight,topRight
};
this.Size.X = WindowSize;
this.Size.Y = WindowSize;
this.Texture2D = graphic;
}
}
}
I refactored my code, and also figured out a solution to this problem. 2 big wins in one. Ok, so I found a solution on CodeProject written in C# which I was readily able to adapt to my project. I'm not sure why I didn't realize this when I posted the question, but what I needed to solve this issue was to create a 'window' of zoom and not think in terms of a 'point zoom'. Yes, even if I am trying to zoom directly into a point, that point is just the center of some sort of a window.
Here is the method I have, which expects start and end mousedown coordinates (screen space), and converts the mandelbrot set window size accordingly.
public void ApplyZoom(double x0, double y0, double x1, double y1)
{
if (x1 == x0 && y0 == y1)
{
//This was just a click, no movement occurred
return;
}
/*
* XMin, YMin and XMax, YMax are the current extent of the set
* mx0,my0 and mx1,my1 are the part we selected
* do the math to draw the selected rectangle
* */
double scaleX, scaleY;
scaleX = (XMax - XMin) / (float)BitmapSize;
scaleY = (YMax - YMin) / (float)BitmapSize;
XMax = (float)x1 * scaleX + XMin;
YMax = (float)y1 * scaleY + YMin;
XMin = (float)x0 * scaleX + XMin;
YMin = (float)y0 * scaleY + YMin;
this.Refresh(); // force mandelbrot to redraw
}
Basically, whats happening is we calculate the ratio between the mandelbrot window size versus the screen size we are drawing to. Then, using that scale, we basically convert our mousedown coordinates to mandelbrot set coordinates (x1*scaleX, etc) and manipulate the current Min and Max coordinates with them, using the Min values as the pivot point.
Here's the link to the CodeProject I used as a reference: CodeProject link
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.
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);
}
}
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();
}
}
}