Determine which control is closest to the mouse pointer - c#

In my C# (.NET 2) app I'd like to determine which control is closet to the mouse.
I can think of a few ways to do this that won't quite work right. I could use the Control.Location property, but that just gives me top/left, and the mouse might be on the other side of the control. I could calculate the center point of a control, but large controls would skew this (being near the edge of a control counts as being close to the control).
So basically I have a bunch of rectangles on a canvas and a point. I need to find the rectangle nearest to the point.
(Ideally I'd like to actually know the distance between the point and rectangle, too).
Any ideas?

You need to find the following:
- Distance to the closest corner
- Distance to the closest edge
- (optionally) distance to the center
Basically, you want the smaller of these three values. Take the min of that for two controls to determine which is closer.
Begin when you load the form by iterating all the controls on the form and creating a collection of the class below.
To find the closest control to a point, iterate the collection (see code at bottom). Keep track of the control with the minimum distance you've found so far. You can test for ContainsPoint() if you want... if you find a control where the point falls within the control bounds, you've got your control (so long as you don't have overlapping controls). Else, when you get to the end of the collection, the control you found with the shortest distance to the center/edge is your control.
public class HitControl {
public Control ThisControl;
private Rectangle ControlBounds;
private Point Center;
public HitControl (Control FormControl) {
ControlBounds = FormControl.Bounds;
Center = new Point(ControlBounds.X + (ControlBounds.Width/2), ControlBounds.Y + (ControlBounds.Height/2));
}
// Calculate the minimum distance from the left, right, and center
public double DistanceFrom(Point TestPoint) {
// Note: You don't need to consider control center points unless
// you plan to allow for controls placed over other controls...
// Then you need to test the distance to the centers, as well,
// and pick the shortest distance of to-edge, to-side, to-corner
bool withinWidth = TestPoint.X > ControlBounds.X && TestPoint.X < ControlBounds.X + ControlBounds.Width;
bool withinHeight = TestPoint.Y > ControlBounds.Y && TestPoint.Y < ControlBounds.Y + ControlBounds.Height;
int EdgeLeftXDistance = Math.Abs(ControlBounds.X - TestPoint.X);
int EdgeRightXDistance = Math.Abs(ControlBounds.X + ControlBounds.Width - TestPoint.X);
int EdgeTopYDistance = Math.Abs(ControlBounds.Y - TestPoint.Y);
int EdgeBottomYDistance = Math.Abs(ControlBounds.Y + ControlBounds.Height - TestPoint.Y);
int EdgeXDistance = Math.Min(EdgeLeftXDistance, EdgeRightXDistance);
int EdgeYDistance = Math.Min(EdgeTopYDistance, EdgeBottomYDistance);
// Some points to consider for rectangle (100, 100, 100, 100):
// - (140, 90): Distance to top edge
// - (105, 10): Distance to top edge
// - (50, 50): Distance to upper left corner
// - (250, 50): Distance to upper right corner
// - (10, 105): Distance to left edge
// - (140, 105): Distance to top edge
// - (105, 140): Distance to left edge
// - (290, 105): Distance to right edge
// - (205, 150): Distance to right edge
// ... and so forth
// You're within the control
if (withinWidth && withinHeight) {
return Math.Min(EdgeXDistance, EdgeYDistance);
}
// You're above or below the control
if (withinWidth) {
return EdgeYDistance;
}
// You're to the left or right of the control
if (withinHeight) {
return EdgeXDistance;
}
// You're in one of the four outside corners around the control.
// Find the distance to the closest corner
return Math.Sqrt(EdgeXDistance ^ 2 + EdgeYDistance ^ 2);
}
public bool ContainsPoint (Point TestPoint) {
return ControlBounds.Contains(TestPoint);
}
}
// Initialize and use this collection
List<HitControl> hitControls = (from Control control in Controls
select new HitControl(control)).ToList();
Point testPoint = new Point(175, 619);
double distance;
double shortestDistance = 0;
HitControl closestControl = null;
foreach (HitControl hitControl in hitControls) {
// Optional... works so long as you don't have overlapping controls
// If you do, comment this block out
if (hitControl.ContainsPoint(testPoint)) {
closestControl = hitControl;
break;
}
distance = hitControl.DistanceFrom(testPoint);
if (shortestDistance == 0 || distance < shortestDistance) {
shortestDistance = distance;
closestControl = hitControl;
}
}
if (closestControl != null) {
Control foundControl = closestControl.ThisControl;
}

First check the point is in any rectangle. If not, you can find the distance between your point and each line segment with the algorithm in this.
You can also find the 4 segments of your control, so you have a list (initiated first time) of four segments (determining the control sides) and now you can find the nearest segment, its nearest rectangle.

You have to think in terms of rectangles :)
Test: Is mouse within control?
If not: How far away from any single edge?
Then you have to know which controls you are interested in, the form is, for example, a control..

For starters, create method that will calculate distance from the rectangle edge to some arbitrary point. Signature for this method should be:
double DistanceFrom(Rect r, Point p);
Then, for the simplest try, iterate all controls, calculate distance, remeber the minimum distance and control that provided it.
For rectangle distance, check this out.
EDIT:
In fact, you can maintain a sorted list of controls so you can always have first one that is closer on top, and maintain that list as the mouse moves - it may prove to be more efficient in the terms of speed. Interesting issue though :)

I agree with Daniel that we need:
double DistanceFrom(Rect r, Point p);
But before that, we need:
double DistanceFrom(Line r, Point p);
and
double AngleBetweenPoints(Point p1, Point p2);

Related

Zoom level by distance in Mapbox map for Xamarin.Forms

Does anyone know how to select the correct zoom level so that two dots hit the screen?
I am using Mapbox and Xamarin.Forms.
In fact, there are more points, but the two most extreme are taken. Using them, I can get the center where the camera will look. Also, of course, I can get the distance between them. But here's how to calculate the specific zoom level (from 0 to 22), I have no idea.
Here is the zoom level documentation.
I was helped by an article on Wikipedia, which some user left in the comments, but then for some reason deleted it. Here
The result is a method:
public const double EARTH_EQUATORIAL_CIRCUMFERENCE_METTERS = 40075016.686; //Equatorial circumference of the Earth
public const double C = EARTH_EQUATORIAL_CIRCUMFERENCE_METTERS;
public static double CalculateZoomLevel(double lat, double distanceBetweenPoints)
{
double logNum = C * Math.Cos(MathHelper.DegreesToRadians(lat)) / distanceBetweenPoints;
double zoomLvl = Math.Log(logNum, 2);
return zoomLvl;
}
It's not perfect, but a very good result. But there is a problem when the path between the points becomes horizontal. In order to avoid this problem, I make some modifications to the distance between the points:
var distance = Distance.BetweenPositions(new Xamarin.Forms.Maps.Position(minLat, minLon),
new Xamarin.Forms.Maps.Position(maxLat, maxLon)).Meters;
if (Math.Abs(minLat - maxLat) < Math.Abs(minLon - maxLon))
{
distance += distance * (Math.Abs(minLon - maxLon) / Math.Abs(minLat - maxLat) / 10);
}
map.ZoomLevel = MapboxHelper.CalculateZoomLevel(zoomPos.Latitude, distance);
zoomPos - Center between two points.
Maybe someone will come in handy.

Find corners/edges on a shape (minimum vertices that can define that shape)

I'm trying to get the corners of the following shape:
By corners I mean this (red dots):
The minimum quantity of points that can define this shape.
And I have implemented the following:
public Shape Optimize()
{
// If the vertices are null or empty this can't be executed
if (vertices.IsNullOrEmpty())
return this; // In this case, return the same instance.
if (!edges.IsNullOrEmpty())
edges = null; //Reset edges, because a recalculation was requested
// The corners available on each iteration
var corners = new Point[] { Point.upperLeft, Point.upperRight, Point.downLeft, Point.downRight };
//The idea is to know if any of the following or previous vertice is inside of the the array from upside, if it is true then we can add it.
Point[] vs = vertices.ToArray();
for (int i = 0; i < vertices.Count - 1; ++i)
{
Point backPos = i > 0 ? vs[i - 1] : vs[vertices.Count - 1],
curPos = vs[i], //Punto actual
nextPos = i < vertices.Count - 1 ? vs[i + 1] : vs[0];
// We get the difference point between the actual point and the back & next point
Point backDiff = backPos - curPos,
nextDiff = nextPos - curPos,
totalDiff = nextPos - backPos;
if (corners.Contains(backDiff) || corners.Contains(nextDiff) || corners.Contains(totalDiff))
AddEdge(curPos, center); // If any of the two points are defined in the corners of the point of before or after it means that the actual vertice is a edge/corner
}
return this;
}
This works rectangled shapes, but rotated shapes are very sharp, so, this code doesn't work well:
Blue pixels (in this photo and the following) are the vertices variable processed on Optimize method.
Green pixels are the detected corners/edges (on both photos).
But sharpness in a shape only defines the side inclination, so what can I do to improve this?
Also, I have tested Accord.NET BaseCornersDetector inherited classes, but the best result is obtained with HarrisCornersDetector, but:
Many edges/corners are innecesary, and they aren't in the needed place (see first photo).
Well, after hours of research I found a library called Simplify.NET that internally runs the Ramer–Douglas–Peucker algorithm.
Also, you maybe interested on the Bresenham algorithm, with this algorithm you can draw a line using two Points.
With this algorithm, you can check if your tolerance is too high, comparing the actual points and the points that this algorithm outputs and making some kind of percentage calculator of similarity.
Finally, is interesting to mention Concave Hull and Convex Hull algorithms.
All this is related to Unity3D.
My outputs:
And my implementation.
It's very important to say, that points needs to be sorted forcing them to be connected. If the shape is concave as you can see on the second photo maybe you need to iterate walls of the shape.
You can see an example of implementation here. Thanks to #Bunny83.

Unity find objects within range of two angles and with a max length (pie slice)

I've been programming an ability for a Hack n Slash which needs to check Units within a pie slice (or inbetween two angles with max length). But I'm stuck on how to check whether an unit is within the arc.
Scenario (Not enough, rep for an image sorry im new)
I currently use Physics2D.OverlapSphere() to get all of the objects within the maximum range. I then loop through all of the found objects to see whether they are within the two angles I specify. Yet this has janky results, probably because angles don't like negative values and value above 360.
How could I make this work or is there a better way to do this?
I probably need to change the way I check whether the angle is within the bounds.
Thanks in advance guys! I might respond with some delay as I won't be at my laptop for a couple hours.
Here is the code snippet:
public static List<EntityBase> GetEntitiesInArc(Vector2 startPosition, float angle, float angularWidth, float radius)
{
var colliders = Physics2D.OverlapCircleAll(startPosition, radius, 1 << LayerMask.NameToLayer("Entity"));
var targetList = new List<EntityBase>();
var left = angle - angularWidth / 2f;
var right = angle + angularWidth / 2f;
foreach (var possibleTarget in colliders)
{
if (possibleTarget.GetComponent<EntityBase>())
{
var possibleTargetAngle = Vector2.Angle(startPosition, possibleTarget.transform.position);
if (possibleTargetAngle >= left && possibleTargetAngle <= right)
{
targetList.Add(possibleTarget.GetComponent<EntityBase>());
}
}
}
return targetList;
}
Vector2.Angle(startPosition, possibleTarget.transform.position);
This is wrong. Imagine a line from the scene origin (0,0) to startPosition and a line to the transform.position. Vector2.Angle is giving you the angle between those two lines, which is not what you want to measure.
What you actually want is to give GetEntitiesInArc a forward vector then get the vector from the origin to the target position (var directionToTarget = startPosition - possibleTarget.transform.position) and measure Vector2.Angle(forward, directionToTarget).

Rotating & printing squares

Got a quick question about an assignment in c#, wpf. The task is to read in an XML file, containing one description for a root panel, and from then on it follows a fixed pattern where each panel has a number of child panels, each of which can have a number of child patterns. Pretty obvious. I can read it in just fine, and traversing the model is no problem.
The thing is: I have to print these panels on a wpf canvas. The relationship between parent and child panels is the following:
the root panel has X Y coordinates to determine its starting point. Other panels do not
each panel, including the root, has a width and height (not necessarily the same)
each panel (except the root panel) has a property 'attachedToSide' which has a value from 0 to 3. The value signifies the side of the parent the child should be placed against.
When printing a panel against a parent panel, we should always put the '0' side of the panel against the parents' side.
So to demonstrate: look at the draft below. The root panel is printed. The root has 4 children. Taking the panel to the right of the root. That panel would have a property attachedToSide='1' to signify it should be stuck against the 1-side of its parent panel. Now since the rule is that the 0 side is the one that should stick to the parent, we have to flip it 90°.
And the story goes on like that.
Now, printing itself is no problem. Where I'm kinda struggling is calculating the actual positions of each square. The first children of the parent are easy, but from then on, I have to do some calculations to position them correctly, based on the previous panel, and I don't want to take the route of nested if-statements. There probably exists a really simple algorithm to fix this, but since I'm not home in that field, I'm struggling a bit. Can anyone give me a nudge in the right direction?
Detail: doing it all purely mvvm too (just for the heck of it), so 0 code in the code-behind. The shapes are an itemcollection with a custom itemspaneltemplate and itemtemplate, I'm doing the rotation by binding the rotation angle to a property in my model.
user3386109's answer shoved me in the right direction, but I got some extra info about the problem that helped me solve this. Take a look at this example:
The parent is printed with the 0-side down (this is standard). It has 3 children: right, top, left. Now, the parent is the only panel for which I receive an X, Y coordinate. That (X,Y) is the center of the 0-side. Additionally I get the width and height. For all children onward, I then get the width, height, and the side of the parent it is on. Since a child should always be connected to its parent with its own 0-side, I can calculate the childs bottom side very easily using the mod-wrapping formule user3386109 already showed:
bottom side child = (bottom side parent + 6 - parents attachment side) % 4
That's the easiest part. Now, one complication is that each child can be wider or less wide than the parent, higher or less high than the parent. That could complicate matters in terms of calculating the top left (X,Y) point from where we need to draw. One thing I always know however, is that the center point of the parent side to which the child is attached, should be the same point as the child side center that is touching that parent (see the red lines on the picture, that'll tell you what I mean).
Now I used the following approach: I decided to calculate coordinates for the top left point, assuming I could draw the child "upright", so with the bottom being the 0-side. Then, I would just rotate along that point.
Using an example:
Notice the parent panel in black. I know from the XML that I need to attach the child panel on side 1 of the parent. Therefor, I calculate the center point of the parents 1 side from its own 0-side center. I know that will be the center of the childs 0-side, since that is where I need to attach them together. I then calculate the childs top left (X,Y) coordinate, which is simple. After that, I can just rotate the child along its center 0-side point. Then we get the following result, where parent and child are connected in the center, and the child is rotated the right way as well.
In short, it's always the same approach:
take the center of the 0-side of a parent (which we will store in each panel object)
relative to that point, calculate where the 0-side center of the child will be
if we have that point, calculate the childs top left point, so we know from where to draw
rotate the child along its 0-side center point (we know the rotation degrees from the side that is at the bottom)
Done. One extra complication was that each child received a certain "offset" value. In short, this is a positive or negative value indicating to push the child to a certain direction (still attached to the parent). This problem is easily solved with just adjusting the right coordinate.
Now, to calculate all the points, it's obvious that it all depends from parent rotation, own rotation and so on. When inspecting the variations, I came to the conclusion that a lot of formulas looked suspiciously similar. The total explanation would require a lot of typing, and frankly I can't be bothered. However: here is the code that creates a child rectangle based on a given parent rectangle, child width, height which side of the parent it should be on, and the offset.
private static Rectangle CreateRectangle(string name, float width, float height, int sideOfParent, float offset, Rectangle parent)
{
Rectangle rect = new Rectangle() { Name = name, Width = width, Height = height, Offset = offset };
// Calculate which side should be at the bottom, depending on the bottom side of the parent,
// and which side of the parent the new rectangle should be attached to
rect.BottomSide = (parent.BottomSide + 6 - sideOfParent) % 4;
// Calculate the bottom mid point of the rectangle
// If | bottom side parent - bottom side child | = 2, just take over the mid bottom point of the parent
if (Math.Abs(parent.BottomSide - rect.BottomSide) == 2) { rect.MidBottom = parent.MidBottom; }
else
{
// Alternative cases
// Formulas for both bottom side parent = 0 or 2 are very similar per bottom side child variation (only plus/minus changes for Y formulas)
// Formulas for both bottom side parent = 1 or 3 are vary similar per bottom side child variation (only plus/minus changes for X formulas)
// Therefor, we create a "mutator" 1 / -1 if needed, to multiply one part of the formula with, so that we either add or subtract
Point parPoint = parent.MidBottom;
if (parent.BottomSide % 2 == 0)
{
// Parent has 0 or 2 at the bottom
int mutator = (parent.BottomSide == 0) ? 1 : -1;
switch (rect.BottomSide % 2 == 0)
{
case true: rect.MidBottom = new Point(parPoint.X, parPoint.Y - (mutator * parent.Height)); break;
case false:
if (rect.BottomSide == 1) rect.MidBottom = new Point(parPoint.X + (parent.Width / 2), parPoint.Y - (mutator * (parent.Height / 2)));
else rect.MidBottom = new Point(parPoint.X - (parent.Width / 2), parPoint.Y - (mutator * (parent.Height / 2)));
break;
}
}
else
{
// Parent has 1 or 3 at the bottom
int mutator = (parent.BottomSide == 1) ? 1 : -1;
switch (rect.BottomSide % 2 == 1)
{
case true: rect.MidBottom = new Point(parPoint.X + (mutator * parent.Height), parPoint.Y); break;
case false:
if (rect.BottomSide == 0) rect.MidBottom = new Point(parPoint.X + (mutator * (parent.Height / 2)), parPoint.Y - (parent.Width / 2));
else rect.MidBottom = new Point(parPoint.X + (mutator * (parent.Height / 2)), parPoint.Y + (parent.Width / 2));
break;
}
}
}
return rect;
}
An example of a real life result of all that:
As I already mentioned, the actual drawing just happens by putting an ItemCollection on a standard grid, binding to the collection of rectangles and setting an appropriate ItemsPanel and ItemTemplate, standard WPF there.
The model for each panel consists of
X,Y coordinates
W,H dimensions
R rotation value (one of four choices)
C a list of up to four children
A attached to side
The rotation value can be encoded as: an angle in degrees, an angle in radians, or just a number between 0 and 3. I would choose the 0 to 3 encoding, where the number represents the side at the bottom. So the root panel has a rotation value of 0.
You are given a complete set of parameters (ignoring A) for the root panel. For all the other panels, you have parameters W,H,C,A but you're missing X,Y,R. So your task is to compute X,Y,R for each panel to complete the model.
Computing the rotation value for the child
Consider the following cases which show the four possible children for each orientation of the parent:
The sequences below the drawings are the child R values, ordered by the child's A value. For example, if the parentR is 0, and the childA is 0, the childR is 2. If parentR is 0 and childA is 1, childR is 1, etc.
First thing to note is that the first number in each sequence is the number at the top of the parent. Second thing to note is that the numbers decrease by 1 (as the childA increases), wrapping to 3 after 0.
So if you take the parent's R value, add 6, and subtract the child's A value, and then apply modulo 4, you get the child's rotation value:
childR = (parentR + 6 - childA) % 4;
Computing the Y value for the child
Note that the location of the child depends primarily on the child's rotation value. If the childR is 0, the child is above the parent. If childR is 1, the child is to the right, etc.
So if the childR is odd, the child has the same Y value as the parent. If the childR is 0, then the childY is the parentY adjusted by the child's height. When the childR is 2, then the childY is the parentY adjusted by either the parent width (parentR odd), or the parent's height (parentR even).
Which results in an if-else chain that looks like this:
if ( childR % 2 ) // odd values, child left or right
childY = parentY
else if ( childR == 0 ) // child above
childY = parentY - childH
else if ( parentR % 2 ) // odd values, adjust by parent width
childY = parentY + parentW
else // even values, adjust by parent height
childY = parentY + parentH
(I'm assuming here that the X,Y coordinate represents the location of the upper-left corner of the panel, and positive Y is down.)
The X calculations are similar to the Y calculations.
So you start at the root, compute X,Y,R for the children of the root, and recursively compute those parameters for each child's children.
That completes your model. Displaying the panels on the view is easy enough, since you have X,Y,W,H,R for each panel.
You could go with a recursive function that print all the children of a panel and you pass said panel as parameters so you have an easy access to the position, the transform etc... Something in the lines of :
public void PrintSelfAndChildren(Panel parent)
{
ApplyTransform();
PrintPanel();
foreach(var child in parent.children)
{
PrintSelfAndChildren(child);
}
}

Approximating an ellipse with a polygon

I am working with geographic information, and recently I needed to draw an ellipse. For compatibility with the OGC convention, I cannot use the ellipse as it is; instead, I use an approximation of the ellipse using a polygon, by taking a polygon which is contained by the ellipse and using arbitrarily many points.
The process I used to generate the ellipse for a given number of point N is the following (using C# and a fictional Polygon class):
Polygon CreateEllipsePolygon(Coordinate center, double radiusX, double radiusY, int numberOfPoints)
{
Polygon result = new Polygon();
for (int i=0;i<numberOfPoints;i++)
{
double percentDone = ((double)i)/((double)numberOfPoints);
double currentEllipseAngle = percentDone * 2 * Math.PI;
Point newPoint = CalculatePointOnEllipseForAngle(currentEllipseAngle, center, radiusX, radiusY);
result.Add(newPoint);
}
return result;
}
This has served me quite while so far, but I've noticed a problem with it: if my ellipse is 'stocky', that is, radiusX is much larger than radiusY, the number of points on the top part of the ellipse is the same as the number of points on the left part of the ellipse.
That is a wasteful use of points! Adding a point on the upper part of the ellipse would hardly affect the precision of my polygon approximation, but adding a point to the left part of the ellipse can have a major effect.
What I'd really like, is a better algorithm to approximate the ellipse with a polygon. What I need from this algorithm:
It must accept the number of points as a parameter; it's OK to accept the number of points in every quadrant (I could iteratively add points in the 'problematic' places, but I need good control on how many points I'm using)
It must be bounded by the ellipse
It must contain the points straight above, straight below, straight to the left and straight to the right of the ellipse's center
Its area should be as close as possible to the area of the ellipse, with preference to optimal for the given number of points of course (See Jaan's answer - appearantly this solution is already optimal)
The minimal internal angle in the polygon is maximal
What I've had in mind is finding a polygon in which the angle between every two lines is always the same - but not only I couldn't find out how to produce such a polygon, I'm not even sure one exists, even if I remove the restrictions!
Does anybody have an idea about how I can find such a polygon?
finding a polygon in which the angle between every two lines is
always the same
Yes, it is possible. We want to find such points of (the first) ellipse quadrant, that angles of tangents in these points form equidistant (the same angle difference) sequence. It is not hard to find that tangent in point
x=a*Cos(fi)
y=b*Sin(Fi)
derivatives
dx=-a*Sin(Fi), dy=b*Cos(Fi)
y'=dy/dx=-b/a*Cos(Fi)/Sin(Fi)=-b/a*Ctg(Fi)
Derivative y' describes tangent, this tangent has angular coefficient
k=b/a*Cotangent(Fi)=Tg(Theta)
Fi = ArcCotangent(a/b*Tg(Theta)) = Pi/2-ArcTan(a/b*Tg(Theta))
due to relation for complementary angles
where Fi varies from 0 to Pi/2, and Theta - from Pi/2 to 0.
So code for finding N + 1 points (including extremal ones) per quadrant may look like (this is Delphi code producing attached picture)
for i := 0 to N - 1 do begin
Theta := Pi/2 * i / N;
Fi := Pi/2 - ArcTan(Tan(Theta) * a/b);
x := CenterX + Round(a * Cos(Fi));
y := CenterY + Round(b * Sin(Fi));
end;
// I've removed Nth point calculation, that involves indefinite Tan(Pi/2)
// It would better to assign known value 0 to Fi in this point
Sketch for perfect-angle polygon:
One way to achieve adaptive discretisations for closed contours (like ellipses) is to run the Ramer–Douglas–Peucker algorithm in reverse:
1. Start with a coarse description of the contour C, in this case 4
points located at the left, right, top and bottom of the ellipse.
2. Push the initial 4 edges onto a queue Q.
while (N < Nmax && Q not empty)
3. Pop an edge [pi,pj] <- Q, where pi,pj are the endpoints.
4. Project a midpoint pk onto the contour C. (I expect that
simply bisecting the theta endpoint values will suffice
for an ellipse).
5. Calculate distance D between point pk and edge [pi,pj].
if (D > TOL)
6. Replace edge [pi,pj] with sub-edges [pi,pk], [pk,pj].
7. Push new edges onto Q.
8. N = N+1
endif
endwhile
This algorithm iteratively refines an initial discretisation of the contour C, clustering points in areas of high curvature. It terminates when, either (i) a user defined error tolerance TOL is satisfied, or (ii) the maximum allowable number of points Nmax is used.
I'm sure that it's possible to find an alternative that's optimised specifically for the case of an ellipse, but the generality of this method is, I think, pretty handy.
I assume that in the OP's question, CalculatePointOnEllipseForAngle returns a point whose coordinates are as follows.
newPoint.x = radiusX*cos(currentEllipseAngle) + center.x
newPoint.y = radiusY*sin(currentEllipseAngle) + center.y
Then, if the goal is to minimize the difference of the areas of the ellipse and the inscribed polygon (i.e., to find an inscribed polygon with maximal area), the OP's original solution is already an optimal one. See Ivan Niven, "Maxima and Minima Without Calculus", Theorem 7.3b. (There are infinitely many optimal solutions: one can get another polygon with the same area by adding an arbitrary constant to currentEllipseAngle in the formulae above; these are the only optimal solutions. The proof idea is quite simple: first one proves that these are the optimal solutions in case of a circle, i.e. if radiusX=radiusY; secondly one observes that under a linear transformation that transforms a circle into our ellipse, e.g. a transformation of multiplying the x-coordinate by some constant, all areas are multiplied by a constant and therefore a maximal-area inscribed polygon of the circle is transformed into a maximal-area inscribed polygon of the ellipse.)
One may also regard other goals, as suggested in the other posts: e.g. maximizing the minimal angle of the polygon or minimizing the Hausdorff distance between the boundaries of the polygon and ellipse. (E.g. the Ramer-Douglas-Peucker algorithm is a heuristic to approximately solve the latter problem. Instead of approximating a polygonal curve, as in the usual Ramer-Douglas-Peucker implementation, we approximate an ellipse, but it is possible to devise a formula for finding on an ellipse arc the farthest point from a line segment.) With respect to these goals, the OP's solution would usually not be optimal and I don't know if finding an exact solution formula is feasible at all. But the OP's solution is not as bad as the OP's picture shows: it seems that the OP's picture has not been produced using this algorithm, as it has less points in the more sharply curved parts of the ellipse than this algorithm produces.
I suggest you switch to polar coordinates:
Ellipse in polar coord is:
x(t) = XRadius * cos(t)
y(t) = YRadius * sin(t)
for 0 <= t <= 2*pi
The problems arise when Xradius >> YRadius (or Yradius >> Yradius)
Instead of using numberOfPoints you can use an array of angles obviously not all identical.
I.e. with 36 points and dividing equally you get angle = 2*pi*n / 36 radiants for each sector.
When you get around n = 0 (or 36) or n = 18 in a "neighborhood" of these 2 values the approx method doesn't works well cause the ellipse sector is significantly different from the triangle used to approximate it. You can decrease the sector size around this points thus increasing precision. Instead of just increasing the number of points that would also increase segments in other unneeded areas. The sequence of angles should become something like (in degrees ):
angles_array = [5,10,10,10,10.....,5,5,....10,10,...5]
The first 5 deg. sequence is for t = 0 the second for t = pi, and again the last is around 2*pi.
Here is an iterative algorithm I've used.
I didn't look for theoretically-optimal solution, but it works quit well for me.
Notice that this algorithm gets as an input the maximal error of the prime of the polygon agains the ellipse, and not the number of points as you wish.
public static class EllipsePolygonCreator
{
#region Public static methods
public static IEnumerable<Coordinate> CreateEllipsePoints(
double maxAngleErrorRadians,
double width,
double height)
{
IEnumerable<double> thetas = CreateEllipseThetas(maxAngleErrorRadians, width, height);
return thetas.Select(theta => GetPointOnEllipse(theta, width, height));
}
#endregion
#region Private methods
private static IEnumerable<double> CreateEllipseThetas(
double maxAngleErrorRadians,
double width,
double height)
{
double firstQuarterStart = 0;
double firstQuarterEnd = Math.PI / 2;
double startPrimeAngle = Math.PI / 2;
double endPrimeAngle = 0;
double[] thetasFirstQuarter = RecursiveCreateEllipsePoints(
firstQuarterStart,
firstQuarterEnd,
maxAngleErrorRadians,
width / height,
startPrimeAngle,
endPrimeAngle).ToArray();
double[] thetasSecondQuarter = new double[thetasFirstQuarter.Length];
for (int i = 0; i < thetasFirstQuarter.Length; ++i)
{
thetasSecondQuarter[i] = Math.PI - thetasFirstQuarter[thetasFirstQuarter.Length - i - 1];
}
IEnumerable<double> thetasFirstHalf = thetasFirstQuarter.Concat(thetasSecondQuarter);
IEnumerable<double> thetasSecondHalf = thetasFirstHalf.Select(theta => theta + Math.PI);
IEnumerable<double> thetas = thetasFirstHalf.Concat(thetasSecondHalf);
return thetas;
}
private static IEnumerable<double> RecursiveCreateEllipsePoints(
double startTheta,
double endTheta,
double maxAngleError,
double widthHeightRatio,
double startPrimeAngle,
double endPrimeAngle)
{
double yDelta = Math.Sin(endTheta) - Math.Sin(startTheta);
double xDelta = Math.Cos(startTheta) - Math.Cos(endTheta);
double averageAngle = Math.Atan2(yDelta, xDelta * widthHeightRatio);
if (Math.Abs(averageAngle - startPrimeAngle) < maxAngleError &&
Math.Abs(averageAngle - endPrimeAngle) < maxAngleError)
{
return new double[] { endTheta };
}
double middleTheta = (startTheta + endTheta) / 2;
double middlePrimeAngle = GetPrimeAngle(middleTheta, widthHeightRatio);
IEnumerable<double> firstPoints = RecursiveCreateEllipsePoints(
startTheta,
middleTheta,
maxAngleError,
widthHeightRatio,
startPrimeAngle,
middlePrimeAngle);
IEnumerable<double> lastPoints = RecursiveCreateEllipsePoints(
middleTheta,
endTheta,
maxAngleError,
widthHeightRatio,
middlePrimeAngle,
endPrimeAngle);
return firstPoints.Concat(lastPoints);
}
private static double GetPrimeAngle(double theta, double widthHeightRatio)
{
return Math.Atan(1 / (Math.Tan(theta) * widthHeightRatio)); // Prime of an ellipse
}
private static Coordinate GetPointOnEllipse(double theta, double width, double height)
{
double x = width * Math.Cos(theta);
double y = height * Math.Sin(theta);
return new Coordinate(x, y);
}
#endregion
}

Categories

Resources