Sunday, June 21, 2009

Bing Maps Silverlight Control Part 4: Drawing Circles To Scale

When you add scaled geometry to the Virtual Earth application you need to remember 2 things:

1) The earth is not flat, so your math needs to account for the curvature of the earth.

2) The Virtual Earth API has, among other things, MapPolygon objects, which are treated as scaled geometry. In my example, I wanted to draw a circle around a point that showed what was on the map 10 miles in any direction. This video shows my reasons why:



I pulled all the parts to build this application from the web. You can find the formulas to calculate distances on a curved surface in Wikipedia: Great Circle Distance
Tim Heuer had a post on calculating the points in a circle using Java: Drawing a Radius
In my project I converted all the code to C# and separated the calculation of locations and the generation of a MapPolygon into two functions.

void AddCircle(Location oLoc, double dRadius, double dOpacity)
{
MapPolygon polygon = new MapPolygon();
polygon.Fill = new SolidColorBrush(Colors.Green);
polygon.Stroke = new SolidColorBrush(Colors.Blue);
polygon.StrokeThickness = 5;
polygon.Opacity = dOpacity;

//this works in miles
polygon.Locations = DrawACircle(oLoc, dRadius);
Map1.AddChild(polygon);
}

public LocationCollection DrawACircle(Location oLoc, double dRadius)
{
var oLocs = new LocationCollection();

var earthRadius = GeoCodeCalc.EarthRadiusInMiles;
var lat = GeoCodeCalc.ToRadian(oLoc.Latitude); //radians
var lon = GeoCodeCalc.ToRadian(oLoc.Longitude); //radians
var d = dRadius / earthRadius; // d = angular distance covered on earths surface

for (int x = 0; x<= 360; x++)
{
var brng = GeoCodeCalc.ToRadian(x); //radians
var latRadians = Math.Asin(Math.Sin(lat) * Math.Cos(d) + Math.Cos(lat) * Math.Sin(d) * Math.Cos(brng));
var lngRadians = lon + Math.Atan2(Math.Sin(brng) * Math.Sin(d) * Math.Cos(lat), Math.Cos(d) - Math.Sin(lat) * Math.Sin(latRadians));

var pt = new Location(180.0 * latRadians / Math.PI, 180.0 * lngRadians / Math.PI);
oLocs.Add(pt);
}
return oLocs;
}

Integrating this with the current UI was straight-forward -- I just added a little more code to the actions the application was already doing. So drawing circles around the current location became this:

void btnCircle_Click(object sender, RoutedEventArgs e)
{
var oMyLoc = from oItem in m_oList
where oItem is MyLocation
select oItem;

PointOfInterest oWhereIAm = oMyLoc.First<PointOfInterest>();
if (oWhereIAm == null)
{
MessageBox.Show("Add Where Am I to the Map");
return;
}

for (int i = 1; i <= 5; i++)
{
double dRadius = i * 10.0;
AddCircle(oWhereIAm.Loc, dRadius, .15);
}
}


And drawing a circle of a given radius about any mouse click became this:



void Map1_MouseClick(object sender, MapMouseEventArgs e)
{
var oLocation = Map1.ViewportPointToLocation(e.ViewportPoint);

var oPOI = new PointOfInterest() { Loc = oLocation };
m_oList.Add(oPOI);

RenderPoint(oPOI);

if (cbDraw.IsChecked == true)
try
{
double dRadius = double.Parse(txtRadius.Text);
AddCircle(oPOI.Loc, dRadius, .5);
}
catch( Exception ex)
{
MessageBox.Show(ex.Message);
}
}


In the next post I will show how to measure between two user-selected points.

2 comments:

Unknown said...

Good to see my old javascript code still kicking around :)
The original source, and a very useful site, for the algorithms is:
http://www.movable-type.co.uk/scripts/LatLong.html
John.

Unknown said...

Nice one. I like how your circles are not scaling with the map (as you zoom in they are bigger and as you zoom out they are smaller). I'm trying to get the same effect with MapPolyline but just can't get it to work.

My MapPolyline is quite simple. I just pass in coordinates to Locations collection and style its stroke n stuff. But it scales with zooming and i don't like it :(