In my C# WinForms application I have a picturebox that hosts 2 curves (Resulted from a voltage/current measurement). The X axis is voltage and Y axis is current. The voltage axis is ranged from -5 to 5 but the current axis is a much smaller scale ranged from -10 uA to 10 uA. The task is to see if the second curve is within 10% of the first curve.
For visual inspection I am trying to draw an envelope around the first curve (Blue one). The curve is just a PointF array. At the moment since I have no idea how to draw a correct envelope around the blue curve, I just draw two other curves that are result of X points of the actual curve added and subtracted by 10% of the original curve. Of course this is a bad approach, but atleast for the section of the curve that is noticably vertical, it works. But as soon as the curve is on its non vertical section, this trick does not work anymore, as you can see in the picture below:
Here is the code that I am using to draw the envelope:
public Bitmap DrawEnvelope(double[,] pinData, float vLimit, float iLimit)
{
g = Graphics.FromImage(box);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
PointF[] u = new PointF[pinData.GetLength(0)]; //Up line
PointF[] d = new PointF[pinData.GetLength(0)]; //Down Line
List<PointF> joinedCurves = new List<PointF>();
float posX = xMaxValue * (vLimit / 100);
float minX = posX * -1;
for (int i = 0; i < pinData.GetLength(0); i++)
{
u[i] = new PointF(400 * (1 + (((float)pinData[i, 0]) + minX) / (xMaxValue + vExpand)), 400 * (1 - ((float)pinData[i, 1] * GetInvers((yMaxValue + iExpand)))));
}
for (int i = 0; i < pinData.GetLength(0); i++)
{
d[i] = new PointF(400 * (1 + (((float)pinData[i, 0]) + posX) / (xMaxValue + vExpand)), 400 * (1 - ((float)pinData[i, 1] * GetInvers((yMaxValue + iExpand)))));
}
Pen pengraph = new Pen(Color.FromArgb(50, 0 ,0 ,200), 1F);
pengraph.Alignment = PenAlignment.Center;
joinedCurves.AddRange(u);
joinedCurves.AddRange(d.Reverse());
PointF[] fillPoints = joinedCurves.ToArray();
SolidBrush fillBrush = new SolidBrush(Color.FromArgb(40, 0, 0, 250));
FillMode newFillMode = FillMode.Alternate;
g.FillClosedCurve(fillBrush, fillPoints, newFillMode, 0);
g.Dispose();
return box;
}
The green circles are added by myself, and they indicate the region that the second curve (Red one) is potentially has a difference bigger than 10% from the orginal curve.
Would be nice if someone put me in the right way, what should I look to to achive a nice envelope around original curve?
UPDATE
Because I am so noob I cant find a way to implement the answers given to this question until now, So put a bounty to see if somone can kindly show me atleast a coding approach to this problem.
You could try finding the gradient between each pair of points and calculating two points either side that are on the orthogonal that passes through the midpoint.
You would then have two more lines defined as a set of points that you could use to draw the envelope.
Your best bet is to iterate your point array and to calculate a perpendicular vector to two consecutive points each time (see Calculating a 2D Vector's Cross Product for implementation clues). Project in either direction along these perpendicular vectors to generate the two point arrays of your envelope.
This function generates them roughly using segment midpoints (as long as the point count is high and your offset is not too small it should look ok when plotted):
private void GetEnvelope(PointF[] curve, out PointF[] left, out PointF[] right, float offset)
{
left = new PointF[curve.Length - 1];
right = new PointF[curve.Length - 1];
for (int i = 1; i < curve.Length; i++)
{
PointF normal = new PointF(curve[i].Y - curve[i - 1].Y, curve[i - 1].X - curve[i].X);
float length = (float)Math.Sqrt(normal.X * normal.X + normal.Y * normal.Y);
normal.X /= length;
normal.Y /= length;
PointF midpoint = new PointF((curve[i - 1].X + curve[i].X) / 2F, (curve[i - 1].Y + curve[i].Y) / 2F);
left[i - 1] = new PointF(midpoint.X - (normal.X * offset), midpoint.Y - (normal.Y * offset));
right[i - 1] = new PointF(midpoint.X + (normal.X * offset), midpoint.Y + (normal.Y * offset));
}
}
It all depends on the way you want the envelop to be sized.
You could calculate/guestimate the slope of the curve in each point by calculating the slope to the next point and the slope to the previous point, average these and then calculate a perpendicular vector to the slope.
Add this vector to the point of the curve; this gives you the right-hand edge of the envelop.
Subtract this vector from the point of the curve; this gives you the left-hand edge of the envelop.
This method will fail if the points are too far apart or very sudden changes in the points appear.
This is probably a dumb suggestion. Perhaps instead of drawing the envelope yourself, maybe you could let winforms do it for you. Try drawing the envelope as a line with a pen that has a larger width. Perhaps it might work.
If you look at this msdn example on varying the pen width, you might see what I mean.
http://msdn.microsoft.com/en-us/library/3bssbs7z.aspx
2 (probably incorrect) possibilities.
Do what you did originally to get the pale blue wide area, but also do it in the vertical direction (not just the horizontal)
Do what Dan suggested with a REALLY thick line (in pale blue) then draw it again, then draw the original (thin) line on top of it.
Related
We know 2 circle's x and y center position, and the radius is the same. I want to visually connect the circles without looping the draw ellipse for each point on the line what connects the 2 circle's center.
From this:
To this:
Code:
int radius = 75;
int x1 = 100;
int y1 = 200;
int x2 = 300;
int y2 = 100;
g.FillEllipse(Brushes.Blue, new Rectangle(x1 - radius / 2, y1 - radius / 2, radius, radius));
g.FillEllipse(Brushes.Blue, new Rectangle(x2 - radius / 2, y2 - radius / 2, radius, radius));
A solution for when the Circles don't have the same Diameter.
The first information needed is the distance between the Centers of two Circles.
To calculate it, we use the Euclidean distance applied to a Cartesian plane:
Where (x1, y1) and (x2, y2) are the coordinates of the Centers of two Circles.
We also need to know the Direction (expressed as a positive or negative value): the calculated [Distance] will always be positive.
in C# it, it can be coded as:
float Direction = (Circle1Center.X > Circle2Center.X) ? -1 : 1;
float Distance = (float)Math.Sqrt(Math.Pow(Circle1Center.X - Circle2Center.X, 2) +
Math.Pow(Circle1Center.Y - Circle2Center.Y, 2));
Distance *= Direction;
Now, we have the Distance between the Centers of two Circles, which also expresses a direction.
We also need to know how this virtual line - connecting the two Centers - is rotated in relation to our drawing plane. In the figure below, the Distance can be viewed as the hypotenuse of a right triangle h = (A, B). The C angle is determined by the intersection of the straight lines, parallel to the axis, that cross the Centers of the Circles.
We need to calculate the angle Theta (θ).
Using the Pythagorean theorem, we can derive that the Sine of the angle Theta is Sinθ = b/h (as in the figure)
Using the Circles' Centers coordinates, this can be coded in C# as:
(Distance is the triangle's hypotenuse)
float SinTheta = (Math.Max(Circle1Center.Y, Circle2Center.Y) -
Math.Min(Circle1Center.Y, Circle2Center.Y)) / Distance;
SinTheta expresses an angle in Radians. We need the angle expressed in Degrees: the Graphics object uses this measure for its world transformation functions.
float RotationAngle = (float)(Math.Asin(SinTheta) * (180 / Math.PI));
Now, we need to build a Connector, a shape that links the 2 Circles. We need a Polygon; a Rectangle can't have different pairs of sides (we are considering Circles with different Diameters).
This Polygon will have the longer sides = to the Distance between the Circles Centers, the shorter sides = to the Circles Diameters.
To build a Polygon, we can use both Graphics.DrawPolygon and GraphicsPath.AddPolygon. I'm choosing the GraphicsPath method, because a GraphicsPath can hold more that one shape and these shapes can interact, in a way.
To connect the 2 considered Circles with a Polygon, we need to rotate the Polygon using the RotationAngle previously calculated.
A simple way to perform the rotation, is to move the world coordinates to the Center of one of the Circles, using the Graphics.TranslateTransform method, then rotate the new coordinates, using Graphics.RotateTransform.
We need to draw our Polygon positioning one of the short sides - corresponding to the Diameter of the Circle which is the center of the coordinates transformation - in the center of the Cirle. Hence, when the rotation will be applied, it's short side it will be in the middle of this transformation, anchored to the Center.
Here, figure 3 shows the positioning of the Polygon (yellow shape) (ok, it looks like a rectangle, never mind);in figure 4 the same Polygon after the rotation.
Notes:
As TaW pointed out, this drawing needs to be performed using a SolidBrush with a non-transparent Color, which is kind of disappointing.
Well, a semi-transparent Brush is not forbidden, but the overlapping shapes will have a different color, the sum of the transparent colors of the intersections.
It is however possible to draw the shapes using a semi-transparent Brush without a Color change, using the GraphicsPath ability to fill its shapes using a color that is applied to all the overlapping parts. We just need to change the default FillMode (see the example in the Docs), setting it to FillMode.Winding.
Sample code:
In this example, two couples of Circles are drawn on a Graphics context. They are then connected with a Polygon shape, created using GraphicsPath.AddPolygon().
(Of course, we need to use the Paint event of a drawable Control, a Form here)
The overloaded helper function accepts both the Circles' centers position, expressed as a PointF and a RectangleF structure, representing the Circles bounds.
This is the visual result, with full Colors and using a semi-transparent brush:
using System.Drawing;
using System.Drawing.Drawing2D;
private float Radius1 = 30f;
private float Radius2 = 50f;
private PointF Circle1Center = new PointF(220, 47);
private PointF Circle2Center = new PointF(72, 254);
private PointF Circle3Center = new PointF(52, 58);
private PointF Circle4Center = new PointF(217, 232);
private void form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.CompositingQuality = CompositingQuality.GammaCorrected;
e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
DrawLinkedCircles(Circle1Center, Circle2Center, Radius1, Radius2, Color.FromArgb(200, Color.YellowGreen), e.Graphics);
DrawLinkedCircles(Circle3Center, Circle4Center, Radius1, Radius2, Color.FromArgb(200, Color.SteelBlue), e.Graphics);
//OR, passing a RectangleF structure
//RectangleF Circle1 = new RectangleF(Circle1Center.X - Radius1, Circle1Center.Y - Radius1, Radius1 * 2, Radius1 * 2);
//RectangleF Circle2 = new RectangleF(Circle2Center.X - Radius2, Circle2Center.Y - Radius2, Radius2 * 2, Radius2 * 2);
//DrawLinkedCircles(Circle1, Circle2, Color.FromArgb(200, Color.YellowGreen), e.Graphics);
}
Helper function:
public void DrawLinkedCircles(RectangleF Circle1, RectangleF Circle2, Color FillColor, Graphics g)
{
PointF Circle1Center = new PointF(Circle1.X + (Circle1.Width / 2), Circle1.Y + (Circle1.Height / 2));
PointF Circle2Center = new PointF(Circle2.X + (Circle2.Width / 2), Circle2.Y + (Circle2.Height / 2));
DrawLinkedCircles(Circle1Center, Circle2Center, Circle1.Width / 2, Circle2.Width / 2, FillColor, g);
}
public void DrawLinkedCircles(PointF Circle1Center, PointF Circle2Center, float Circle1Radius, float Circle2Radius, Color FillColor, Graphics g)
{
float Direction = (Circle1Center.X > Circle2Center.X) ? -1 : 1;
float Distance = (float)Math.Sqrt(Math.Pow(Circle1Center.X - Circle2Center.X, 2) +
Math.Pow(Circle1Center.Y - Circle2Center.Y, 2));
Distance *= Direction;
float SinTheta = (Math.Max(Circle1Center.Y, Circle2Center.Y) -
Math.Min(Circle1Center.Y, Circle2Center.Y)) / Distance;
float RotationDirection = (Circle1Center.Y > Circle2Center.Y) ? -1 : 1;
float RotationAngle = (float)(Math.Asin(SinTheta) * (180 / Math.PI)) * RotationDirection;
using (GraphicsPath path = new GraphicsPath(FillMode.Winding))
{
path.AddEllipse(new RectangleF(-Circle1Radius, -Circle1Radius, 2 * Circle1Radius, 2 * Circle1Radius));
path.AddEllipse(new RectangleF(-Circle2Radius + (Math.Abs(Distance) * Direction),
-Circle2Radius, 2 * Circle2Radius, 2 * Circle2Radius));
path.AddPolygon(new[] {
new PointF(0, -Circle1Radius),
new PointF(0, Circle1Radius),
new PointF(Distance, Circle2Radius),
new PointF(Distance, -Circle2Radius),
});
path.AddEllipse(new RectangleF(-Circle1Radius, -Circle1Radius, 2 * Circle1Radius, 2 * Circle1Radius));
path.AddEllipse(new RectangleF(-Circle2Radius + (Math.Abs(Distance) * Direction),
-Circle2Radius, 2 * Circle2Radius, 2 * Circle2Radius));
path.CloseAllFigures();
g.TranslateTransform(Circle1Center.X, Circle1Center.Y);
g.RotateTransform(RotationAngle);
using (SolidBrush FillBrush = new SolidBrush(FillColor)) {
g.FillPath(FillBrush, path);
}
g.ResetTransform();
}
}
As the other answers so far slightly miss the correct solution, here is one that connects two circles of equal size:
using (Pen pen = new Pen(Color.Blue, radius)
{ EndCap = LineCap.Round, StartCap = LineCap.Round } )
g.DrawLine(pen, x1, y1, x2, y2);
Notes:
Usually is is good idea to set the smoothing mode of the graphics object to anti-alias..
To connect two circles of different sizes will take some math to calculate the four outer tangent points. From these one can get a polygon to fill or, if necessary one could create a GraphicsPath to fill, in case the color has an alpha < 1.
Jimi's comments point to a different solution that make use of GDI+ transformation capabilities.
Some of the answers or comments refer to the desired shape as an oval. While this ok in common speech, here, especially when geometry books are mentioned, this is wrong, as an oval will not have any straight lines.
As Jimi noted, what you call radius is really the diameter of the circles. I left the wrong term in the code but you should not!
Pseudo style:
circle1x;
circle1y;
circle2x;
circle2y;
midx=circle1x-circle2x;
midy=circle2x-circle2x;
draw circle at midx midy;
repeat for midx midy, in both directions. add another circle. honestly man, this isnt worth it,in order to make it smooth, you will need several circles. you need to draw an oval using the center of both circles as the two centers of your oval
Currently I have the following code:
AdjustableArrowCap arrow = new AdjustableArrowCap(10, 15, false);
penStateOutline.CustomEndCap = arrow;
And it draws this:
I have tried all day to make the arrow point to the ellipse itself rather than the center of it..
Update (I was wrong about the cap extending the line; it doesn't!)
To let the line and its cap end at the cirlce's outside you need to make the line shorter by the radius of the circle.
There are two approaches:
You can find a new endpoint that sits on the circle by calculating it, either with Pythagoras or by trigonometry. Then replace the endpoint, i.e. the circle's center, when drawing the line or curve by that new point.
Or put the other way round: You need to calculate a point on the circle as the new endpoint of the line.
It requires a little math, unless the line is horizontal or vertical...
This will work well for straight lines but for curves it may cause considerable changes in the shape, depending on how close the points get and how curved the shape is.
This may or may not be a problem.
To avoid it you can replace the curve points by a series of line points that are close enough to look like a curve when drawn. From the list of points we subtract all those that do not lie inside the circle.
This sounds more complicated than it is as there is a nice class called GraphicsPath that will allow you to add a curve and then flatten it. The result is a more or less large number of points. The same class also allows you to determine whether a point lies inside a shape, i.e. our case inside the circle.
To implement the latter approach, here is a routine that transforms a list of curve points to a list of line points that will end close to the circle..:
void FlattenCurveOutside(List<Point> points, float radius)//, int count)
{
using (GraphicsPath gp = new GraphicsPath())
using (GraphicsPath gpc = new GraphicsPath())
{
// firt create a path that looks like our circle:
PointF l = points.Last();
gpc.AddEllipse(l.X - radius, l.Y - radius, radius * 2, radius* 2);
// next one that holds the curve:
gp.AddCurve(points.ToArray());
// now we flatten it to a not too large number of line segments:
Matrix m = new Matrix(); // dummy matrix
gp.Flatten(m, 0.75f); // <== play with this param!!
// now we test the pathpoints from bach to front until we have left the circle:
int k = -1;
for (int i = gp.PathPoints.Length - 1; i >= 0; i--)
{
if ( !gpc.IsVisible(gp.PathPoints[i])) k = i;
if (k > 0) break;
}
// now we know how many pathpoints we want to retain:
points.Clear();
for (int i = 1; i <= k; i++)
points.Add(Point.Round(gp.PathPoints[i]));
}
}
Note that when the last part of the curve is too straight the result may look a little jagged..
Update 2
To implement the former approach here is a function that returns a PointF on a circle of radius r and a line connecting a Point b with the circle center c:
PointF Intersect(Point c, Point a, int rad)
{
float dx = c.X - a.X;
float dy = c.Y - a.Y;
var radians = Math.Atan2(dy, dx);
var angle = 180 - radians * (180 / Math.PI);
float alpha = (float)(angle * Math.PI / 180f);
float ry = (float)(Math.Sin(alpha) * rad );
float rx = (float)(Math.Cos(alpha) * rad );
return new PointF(c.X + rx, c.Y - ry);
}
The result thankfully looks rather similar:
The math can be simplified a little..
To apply it you can get the new endpoint and replace the old one:
PointF I = Intersect(c, b, r);
points2[points2.Count - 1] = Point.Round(I);
Thank you so much to #TaW for helping.
Unfortunately the answer he provided did not match my stupid OCD..
You made me think about the problem from a different perspective and I now have found a solution that I'll share if anyone else needs, note that the solution is not perfect.
public static Point ClosestPointOnCircle(int rad, PointF circle, PointF other)
{
if (other.X == circle.X && other.Y == circle.Y) // dealing with division by 0
other.Y--;
return new Point((int)Math.Floor(circle.X + rad * ((other.X - circle.X) / (Math.Sqrt(Math.Pow(other.X - circle.X, 2) + Math.Pow(other.Y - circle.Y, 2))))), (int)Math.Floor(circle.Y + rad * ((other.Y - circle.Y) / (Math.Sqrt(Math.Pow(other.X - circle.X, 2) + Math.Pow(other.Y - circle.Y, 2))))));
}
What the code does is return a point on the circle that is the closest to the another point, then, I used the curve middle point as the other point to change the end point of the curve.
Then I used the arrow cap as normal and got this:image
Which is good enough for my project.
i want to render nice radial tree layout and a bit stumbled with curved edges. The problem is that with different angles between source and target points the edges are drawn differently. Provided pics are from the single graph so you can see how they're differ for different edge directions. I think the point is in beizer curve control points generation and i just can't understand how to fix them.
I want them to be drawn the same way no matter what's the direction of the edge.
How can i achieve this as in Pic1?
How can i achieve this as in Pic2?
Like here: https://bl.ocks.org/mbostock/4063550
Thank you!
Code:
//draw using DrawingContext of the DrawingVisual
//gen 2 control points
double dx = target.X - source.X, dy = target.Y - source.Y;
var pts = new[]
{
new Point(source.X + 2*dx/3, source.Y),
new Point(target.X - dx/8, target.Y - dy/8)
};
//get geometry
var geometry = new StreamGeometry { FillRule = FillRule.EvenOdd };
using (var ctx = geometry.Open())
{
ctx.BeginFigure(START_POINT, false /* is filled */, false /* is closed */);
ctx.BezierTo(pts[0], pts[1], END_POINT, true, false);
}
geometry.Freeze();
//draw it
dc.DrawGeometry(DrawingBrush, DrawingPen, geometry);
UPDATE 1:
I've got the angle between previous vertex and source in radians using the following formula: Math.Atan2(prev.Y - source.Y, source.X - prev.X);
But still i get the edges like in Pic.4.
UPDATE 2
The prev vertex pos for branchAngle calculation is inaccurate so i decided to take an average angle between all edges in a branch as the branchAngle. This approach fails when edges from one brach are around the 180 deg mark and branch can have edge angles like 175, 176.. -176!! I use this code to make them all positive:
var angle = Math.Atan2(point1.Y - point2.Y, point1.X - point2.X);
while (angle < 0d)
angle += Math.PI*2;
But now the angles can be 350, 359.. 2!!! Quite difficult to calc an average :) Can you please advice me how i can work this around?
Pic1
Pic2
Pic3
Pic4
Looking at the graph from the link you provided each branch in the tree has it's own angle, which is used to declare the control points of the branch. This branchAngle is the same as the one of the vector going from the first node to the previous one (every branch can spawn several branches in turn). The angle of the first branch (first node = previous node = center) seems to be around -60°.
Setting the type of curve can be done by compensating this branch angle (0°, -90°, -180°,...) for all branches in the tree. Resulting in the controlAngle used for laying out the control points.
Generating the control points while taking into account the angles:
//gen per branch
double branchAngle = 30 * Math.PI / 180; //e.g., calc vector angle here
double cosB = Math.Cos(branchAngle);
double sinB = Math.Sin(branchAngle);
//depending on the desired curve compensate -90°, -180°,...
double controlAngle = branchAngle - (90 * Math.PI / 180);
double cosA = Math.Cos(controlAngle);
double sinA = Math.Sin(controlAngle);
//gen 2 control points
//calculate dx dy after rotation with branchAngle
double dxbase = target.X - source.X, dybase = target.Y - source.Y;
double dx = dxbase*sinB - dybase*cosB
double dy = dxbase*cosB + dybase*sinB
//control points based on controlAngle
var pts = new[]
{
new Point(source.X + (2*dx/3)*cosA , source.Y + (2*dx/3)*sinA),
new Point(target.X - (dx/8)*cosA + (dy/8)*sinA, target.Y - (dx/8)*sinA - (dy/8)*cosA)
};
Quick check
branchAngle = 30° &
compensation = -90° ->
controlAngle = -60°
I've spent a few hours today researching how random terrain generation tends to be done and after reading about the plasma fractal (midpoint displacement and diamond square algo's) I decided to try and have a go at implementing one. My result was actually not terriable, but I have these horrible square/line/grid type artefacts that I just can not seem to get rid of!
When rendered as a gray scale image my height map looks something like:
height map http://sphotos-d.ak.fbcdn.net/hphotos-ak-ash3/535816_10151739010123327_225111175_n.jpg
Obviously there is a fair amount of code involved in this but I will try to post what is only relevant. I've not not posted the code that turns it into a texture for example, but do not worry I have already tried just filling my height array with a smooth gradient and the texture comes out fine :)
I begin by setting the four corners of the map to random values between 0 and 1 and then start the recursive displacement algo:
public void GenerateTerrainLayer()
{
//set the four corners of the map to have random values
TerrainData[0, 0] = (float)RandomGenerator.NextDouble();
TerrainData[GenSize, 0] = (float)RandomGenerator.NextDouble();
TerrainData[0, GenSize] = (float)RandomGenerator.NextDouble();
TerrainData[GenSize, GenSize] = (float)RandomGenerator.NextDouble();
//begin midpoint displacement algorithm...
MidPointDisplace(new Vector2_I(0, 0), new Vector2_I(GenSize, 0), new Vector2_I(0, GenSize), new Vector2_I(GenSize, GenSize));
}
TerrainData is simply a 2D array of floats*. Vector2_I is just my own integer vector class. The last four functions are MidPointDisplace which is the recursive function, CalculateTerrainPointData which averages 2 data values and adds some noise, CalculateTerrainPointData2 which averages 4 data values and adds some noise and has a slightly higher scale value (its only used for center points) and finally my noise function which atm is just some random noise and not a real noise like perlin etc. They look like this:
private void MidPointDisplace(Vector2_I topleft, Vector2_I topright, Vector2_I bottomleft, Vector2_I bottomright)
{
//check size of square working on.. if its shorter than a certain amount stop the algo, we've done enough
if (topright.X - topleft.X < DisplacementMaxLOD)
{
return;
}
//calculate the positions of all the middle points for the square that has been passed to the function
Vector2_I MidLeft, MidRight, MidTop, MidBottom, Center;
MidLeft.X = topleft.X;
MidLeft.Y = topleft.Y + ((bottomleft.Y - topleft.Y) / 2);
MidRight.X = topright.X;
MidRight.Y = topright.Y + ((bottomright.Y - topright.Y) / 2);
MidTop.X = topleft.X + ((topright.X - topleft.X) / 2);
MidTop.Y = topleft.Y;
MidBottom.X = bottomleft.X + ((bottomright.X - bottomleft.X) / 2);
MidBottom.Y = bottomleft.Y;
Center.X = MidTop.X;
Center.Y = MidLeft.Y;
//collect the existing data from the corners of the area passed to algo
float TopLeftDat, TopRightDat, BottomLeftDat, BottomRightDat;
TopLeftDat = GetTerrainData(topleft.X, topleft.Y);
TopRightDat = GetTerrainData(topright.X, topright.Y);
BottomLeftDat = GetTerrainData(bottomleft.X, bottomleft.Y);
BottomRightDat = GetTerrainData(bottomright.X, bottomright.Y);
//and the center
//adverage data and insert for midpoints..
SetTerrainData(MidLeft.X, MidLeft.Y, CalculateTerrainPointData(TopLeftDat, BottomLeftDat, MidLeft.X, MidLeft.Y));
SetTerrainData(MidRight.X, MidRight.Y, CalculateTerrainPointData(TopRightDat, BottomRightDat, MidRight.X, MidRight.Y));
SetTerrainData(MidTop.X, MidTop.Y, CalculateTerrainPointData(TopLeftDat, TopRightDat, MidTop.X, MidTop.Y));
SetTerrainData(MidBottom.X, MidBottom.Y, CalculateTerrainPointData(BottomLeftDat, BottomRightDat, MidBottom.X, MidBottom.Y));
SetTerrainData(Center.X, Center.Y, CalculateTerrainPointData2(TopLeftDat, TopRightDat, BottomLeftDat, BottomRightDat, Center.X, Center.Y));
debug_displacement_iterations++;
//and recursively fire off new calls to the function to do the smaller squares
Rectangle NewTopLeft = new Rectangle(topleft.X, topleft.Y, Center.X - topleft.X, Center.Y - topleft.Y);
Rectangle NewTopRight = new Rectangle(Center.X, topright.Y, topright.X - Center.X, Center.Y - topright.Y);
Rectangle NewBottomLeft = new Rectangle(bottomleft.X, Center.Y, Center.X - bottomleft.X, bottomleft.Y - Center.Y);
Rectangle NewBottomRight = new Rectangle(Center.X , Center.Y, bottomright.X - Center.X, bottomright.Y - Center.Y);
MidPointDisplace(new Vector2_I(NewTopLeft.Left, NewTopLeft.Top), new Vector2_I(NewTopLeft.Right, NewTopLeft.Top), new Vector2_I(NewTopLeft.Left, NewTopLeft.Bottom), new Vector2_I(NewTopLeft.Right, NewTopLeft.Bottom));
MidPointDisplace(new Vector2_I(NewTopRight.Left, NewTopRight.Top), new Vector2_I(NewTopRight.Right, NewTopRight.Top), new Vector2_I(NewTopRight.Left, NewTopRight.Bottom), new Vector2_I(NewTopRight.Right, NewTopRight.Bottom));
MidPointDisplace(new Vector2_I(NewBottomLeft.Left, NewBottomLeft.Top), new Vector2_I(NewBottomLeft.Right, NewBottomLeft.Top), new Vector2_I(NewBottomLeft.Left, NewBottomLeft.Bottom), new Vector2_I(NewBottomLeft.Right, NewBottomLeft.Bottom));
MidPointDisplace(new Vector2_I(NewBottomRight.Left, NewBottomRight.Top), new Vector2_I(NewBottomRight.Right, NewBottomRight.Top), new Vector2_I(NewBottomRight.Left, NewBottomRight.Bottom), new Vector2_I(NewBottomRight.Right, NewBottomRight.Bottom));
}
//helper function to return a data value adveraged from two inputs, noise value added for randomness and result clamped to ensure a good value
private float CalculateTerrainPointData(float DataA, float DataB, int NoiseX, int NoiseY)
{
return MathHelper.Clamp(((DataA + DataB) / 2.0f) + NoiseFunction(NoiseX, NoiseY), 0.0f, 1.0f) * 1.0f;
}
//helper function to return a data value adveraged from four inputs, noise value added for randomness and result clamped to ensure a good value
private float CalculateTerrainPointData2(float DataA, float DataB, float DataC, float DataD, int NoiseX, int NoiseY)
{
return MathHelper.Clamp(((DataA + DataB + DataC + DataD) / 4.0f) + NoiseFunction(NoiseX, NoiseY), 0.0f, 1.0f) * 1.5f;
}
private float NoiseFunction(int x, int y)
{
return (float)(RandomGenerator.NextDouble() - 0.5) * 0.5f;
}
Ok thanks for taking the time to look - hopefully someone knows where this grid-like pattern is appearing from :)
*edit - accidently wrote ints, corrected to floats
I identified 3 problems in your code. (2 of which are related)
You don't scale down the randomness in each step. There must be a reduction of the randomness in each step. Otherwise you get white(-ish) noise. You choose a factor (0.5-0.7 worked fine for my purposes) and multiply the reduction by alpha in each recursion and scale the generated random number by that factor.
You swapped the diamond and square step. First the diamonds, then the squares. The other way round is impossible (see next).
Your square step uses only points in one direction. This one probably causes the rectangular structures you are talking about. The squares must average the values to all four sides. This means that the square step depends on the point generated by the diamond step. And not only the diamond step of the rectangle you are currently looking at, also of the rectangles next to it. For values outside of the map, you can either wrap, use a fixed value or only average 3 values.
I see a problem in your CalculateTerrainPointData implementation: you're not scaling down the result of NoiseFunction with each iteration.
See this description of the Midpoint Displacement algorithm:
Start with a single horizontal line segment.
Repeat for a sufficiently large number of times:
Repeat over each line segment in the scene:
Find the midpoint of the line segment.
Displace the midpoint in Y by a random amount.
Reduce the range for random numbers.
A fast way to do it in your code without changing too much is by adding some scale parameter to MidPointDisplace (with default set to 1.0f) and CalculateTerrainPointData; use it in CalculateTerrainPointData to multiply result of NoiseFunction; and reduce it with each recursive call to MidPointDisplace(..., 0.5f * scale).
Not sure though if that is the only cause to your image looking wrong or there are other problems.
According to Wikipedia's summary of midpoint displacement, only the average for the center most point get noise added to it - try only adding noise via CalculateTerrainPointData2 & removing the noise in CalculateTerrainPointData.
I am building a Tile Overlay server for Google maps in C#, and have found a few different code examples for calculating Y from Latitude. After getting them to work in general, I started to notice certain cases where the overlays were not lining up properly. To test this, I made a test harness to compare Google Map's Mercator LatToY conversion against the formulas I found online. As you can see below, they do not match in certain cases.
Case #1
Zoomed Out: The problem is most
evident when zoomed out. Up close,
the problem is barely visible.
Case #2
Point Proximity to Top & Bottom of
viewing bounds: The problem is worse
in the middle of the viewing bounds,
and gets better towards the edges.
This behavior can negate the behavior
of Case #1
The Test:
I created a google maps page to
display red lines using the Google Map
API's built in Mercator conversion,
and overlay this with an image using
the reference code for doing Mercator
conversion. These conversions are
represented as black lines. Compare
the difference.
The Results:
Equator http://www.kayak411.com/Mercator/MercatorComparison%20-%20Equator.png
North Zoomed Out http://www.kayak411.com/Mercator/MercatorComparison%20-%20North%20Zoomed%20Out.png
Check out the top-most and bottom-most lines:
North Top & Bottom Example http://www.kayak411.com/Mercator/MercatorComparison%20-%20North%20Zoomed%20Out%20-%20TopAndBottom.png
The problem gets visually larger but numerically smaller as you zoom in:
alt text http://www.kayak411.com/Mercator/MercatorComparison%20-%20North%20Zoomed%20Midway.png
And it all but disappears at closer zoom levels, regardless of screen orientation.
alt text http://www.kayak411.com/Mercator/MercatorComparison%20-%20North%20Zoomed%20In.png
The Code:
Google Maps Client Side Code:
var lat = 0;
for (lat = -80; lat <= 80; lat += 5) {
map.addOverlay(new GPolyline([new GLatLng(lat, -180), new GLatLng(lat, 0)], "#FF0033", 2));
map.addOverlay(new GPolyline([new GLatLng(lat, 0), new GLatLng(lat, 180)], "#FF0033", 2));
}
Server Side Code:
Tile Cutter :
http://mapki.com/wiki/Tile_Cutter
OpenStreetMap Wiki :
http://wiki.openstreetmap.org/wiki/Mercator
protected override void ImageOverlay_ComposeImage(ref Bitmap ZipCodeBitMap)
{
Graphics LinesGraphic = Graphics.FromImage(ZipCodeBitMap);
Int32 MapWidth = Convert.ToInt32(Math.Pow(2, zoom) * 255);
Point Offset =
Cartographer.Mercator2.toZoomedPixelCoords(North, West, zoom);
TrimPoint(ref Offset, MapWidth);
for (Double lat = -80; lat <= 80; lat += 5)
{
Point StartPoint = Cartographer.Mercator2.toZoomedPixelCoords(lat, -179, zoom);
Point EndPoint = Cartographer.Mercator2.toZoomedPixelCoords(lat, -1, zoom);
TrimPoint(ref StartPoint, MapWidth);
TrimPoint(ref EndPoint, MapWidth);
StartPoint.X = StartPoint.X - Offset.X;
EndPoint.X = EndPoint.X - Offset.X;
StartPoint.Y = StartPoint.Y - Offset.Y;
EndPoint.Y = EndPoint.Y - Offset.Y;
LinesGraphic.DrawLine(new Pen(Color.Black, 2),
StartPoint.X,
StartPoint.Y,
EndPoint.X,
EndPoint.Y);
LinesGraphic.DrawString(
lat.ToString(),
new Font("Verdana", 10),
new SolidBrush(Color.Black),
new Point(
Convert.ToInt32((width / 3.0) * 2.0),
StartPoint.Y));
}
}
protected void TrimPoint(ref Point point, Int32 MapWidth)
{
point.X = Math.Max(point.X, 0);
point.X = Math.Min(point.X, MapWidth - 1);
point.Y = Math.Max(point.Y, 0);
point.Y = Math.Min(point.Y, MapWidth - 1);
}
So, Anyone ever experienced this? Dare I ask, resolved this? Or simply have a better C# implementation of Mercator Project coordinate conversion?
Thanks!
Thank you all for your suggestions & assistance.
What I eventually found out is that it's not a formula or technical problem, I believe it's a methodology problem.
You can't define the viewing area in Lat/Lng format, and expect to populate it with the appropriate Mercator projections. That's where the distortion happens. Instead, you have to define the correct viewing box in Mercator, and project Mercator.
Doing that I was able to correctly match up with Google maps.
You may have to create several points along the longtitude in order for the points to be projected corretly along the latitude. In your examples you are only really projecting two points at the start and the end of the line and connecting the two.
The problem will be more apparent at the equator due to the more significant curvature of the earth. It will be less when zoomed in for the same reason.
Have a look at http://code.google.com/apis/maps/documentation/overlays.html#Great_Circles
Try creating your Google polylines with the geodsic parameter to see if this makes a difference. I think this adds points along the line and projects them automatically:
var lat = 0;
var polyOptions = {geodesic:true};
for (lat = -80; lat <= 80; lat += 5) {
map.addOverlay(new GPolyline([new GLatLng(lat, -180), new GLatLng(lat, 0)], "#FF0033", 2, polyOptions));
map.addOverlay(new GPolyline([new GLatLng(lat, 0), new GLatLng(lat, 180)], "#FF0033", 2, polyOptions));
}
I had to read into this as all my distance measurements were wrong in OpenLayers for similar reasons: http://geographika.co.uk/watch-out-for-openlayer-distances (more links/explanations)