Monday, June 22, 2009

Virtual Earth Rebranded to Bing Maps

We've been doing a series of posts on Silverlight 3 and Virtual Earth.

Microsoft has recently rebranded Virtual Earth, renaming it "Bing Maps". Read about their thinking here:

http://www.bing.com/community/blogs/maps/archive/2009/05/28/rebranding-microsoft-virtual-earth-to.aspx

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:

video

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.

Tuesday, June 16, 2009

Virtual Earth Silverlight Control Part 3: Locating Best Buy Stores

I was searching the web for sources of Lat/Long data I could mash-up to in a Virtual Earth application and I ran across Best Buy ReMix. The API to collect store location information was easy to use and returned Lat/Long info, so I integrated it into the application. Watch this video to see how I did it:

video

I like to start by testing the URL in the browser:


http://api.remix.bestbuy.com/v1/stores(area(44236,20))?apiKey=[YourKey]


The Remix API returned XML describing the stores within 20 miles of my zipcode. Here is some of what I got back:


<stores currentPage="1" totalPages="1" from="1" to="6" total="6" queryTime="0.004" totalTime="0.013" canonicalUrl="/v1/stores(area("44236",20))?apiKey=fbsn9dsqvgnm42vyk4x5hman">
<store>
<storeId>758</storeId>
<name>Macedonia OH</name>
<address>470 East Aurora Road</address>
<city>Macedonia</city>
<region>OH</region>
<postalCode>44056</postalCode>
<fullPostalCode>44056-1834</fullPostalCode>
<country>US</country>
<lat>41.313461</lat>
<lng>-81.521233</lng>
<phone>330-468-6850</phone>
<hours>10:00am - 9:00pm Monday - Friday, 11:00am - 6:00pm Saturday, 11:00am - 6:00pm Sunday</hours>
<distance>6.0</distance>
</store>


The next step is to verify that Silverlight can consume this data using the WebClient Class. The code is simple but it does not always work, based on permissions (see Tim Heuer's explanation of this). I test it with this code:


void LoadBestBuyStores()
{
WebClient oClient = new WebClient();

string sUri = string.Format("http://api.remix.bestbuy.com/v1/stores(area({0},20))?apiKey=fbsn9dsqvgnm42vyk4x5hman", txtZipCode.Text);
var oAddress = new Uri(sUri);
oClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(BestBuyStores_DownloadStringCompleted);
oClient.DownloadStringAsync(oAddress);
}

void BestBuyStores_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
string sResult = e.Result;
MessageBox.Show(sResult);
}



To finish the application, I needed to parse the XML that was returned and map it into an object instance, so I defined a class 'BestBuyStores':



public class BestBuyStore : PointOfInterest
{
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string Phone { get; set; }
public string Hours { get; set; }
}


and then added LinqToXML code to build my objects:


void BestBuyStores_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
if (e.Error != null )
return;

string sResult = e.Result;

var doc = XDocument.Parse(sResult);

var oStores = from element in doc.Descendants("store")
select new BestBuyStore
{
Name = element.Element("name").Value,
Address = element.Element("address").Value,
City = element.Element("city").Value,
Region = element.Element("region").Value,
Phone = element.Element("phone").Value,
Hours = element.Element("hours").Value,
Loc = new Location(double.Parse(element.Element("lat").Value), double.Parse(element.Element("lng").Value))
};

foreach (BestBuyStore oStore in oStores.ToList<BestBuyStore>())
{
RenderPoint(oStore);
m_oList.Add(oStore);
}

frmBestBuy.Visibility = Visibility.Visible;
frmBestBuy.ItemsSource = oStores;
}


Finally by extending the RenderPoint method I was able to mark each location with a custom Best Buy graphic:


void RenderPoint(PointOfInterest oPOI)
{
if (oPOI is MyLocation)
{
Uri oSource = new Uri(@"Pushpin.png", UriKind.Relative);
var oBitmap = new BitmapImage(oSource);
var oImage = new Image() { Source = oBitmap, Width=23, Height=27 };
m_oLayer.AddChild(oImage, oPOI.Loc, PositionMethod.BottomCenter);
}
else if (oPOI is BestBuyStore)
{
Uri oSource = new Uri(@"BestBuy.png", UriKind.Relative);
var oBitmap = new BitmapImage(oSource);
var oImage = new Image() { Source = oBitmap, Width = 23, Height = 27 };
m_oLayer.AddChild(oImage, oPOI.Loc, PositionMethod.BottomLeft);
}
else
{
var oRect = new System.Windows.Shapes.Rectangle() { Fill = new SolidColorBrush(Colors.Red), Width = 10, Height = 10 };
m_oLayer.AddChild(oRect, oPOI.Loc, PositionMethod.Center);
}
}

Tuesday, June 9, 2009

Generic Type Conversion

While working on the Virtual Earth map application, I discovered the need to parse data from a string of characters. So I searched the net and found this answer on stack overflow: generic-type-conversion-from-string . I like this code example because it looks like the simplest and most general approach:

public class TypedProperty<T> : Property
{
public T TypedValue
{
get { return (T)Convert.ChangeType(base.Value, typeof(T)); }
set { base.Value = value.ToString();}
}
}
But if you cut and paste the code into a Silverlight 2 or Silverlight 3 application it does not run. This is because the two argument version of the ChangeType method is not part of the Silverlight runtime. I guess I could have used the three argument version, but instead I implemented a function the uses reflection:


public bool TryParse<T>(string sValue, out T oValue) where T : IConvertible
{
oValue = default(T);

try
{
string[] parts = sValue.Split(':');
MethodInfo oMethod = typeof(T).GetMethod("Parse", new Type[] { typeof(string) } );
oValue = (T)oMethod.Invoke(null, new object[] { parts[1].Trim() });
return true;
}
catch { }
return false;
}
Silverlight Size vs. Functionality

You find this all the time in Silverlight development -- some of the tools, functions and features you have come to rely on in .NET are just not part of the lightweight Silverlight runtime. I read on one blog that programming with Silverlight is like going camping with .NET. In fact, I first learned this the hard way trying to recompile some previous .NET code. I couldn't include the existing library -- it had to be rebuilt using the Silverlight libraries. When I attempted to recompile it, I found that it needed to be reengineered to use functionality available in the Silverlight runtime.

I have always been able to find a creative solution to simple programming issues like the one above, and I think the ability to deliver my solution over the web to any machine, without creating a custom install, makes it worth the effort to find these alternative approaches.

Sunday, June 7, 2009

Virtual Earth Silverlight Control Part 2: Locate Me

It was a post by Nikhil Kothari that inspired me to create a method to report my current location on the Virtual Earth Silverlight control.

http://www.nikhilk.net/Silverlight-Locate-Me.aspx

While I was traveling around the country (see http://www.wiredwalkabout.com/), I would run Nikhil's application to see the result. For the most part, it always reported my location about 20 miles from where I really was, which I think is a problem with the web service used and not the code calling it.

http://api.hostip.info/get_html.php?position=true




Nikhil's current live sample has been updated, but it is still not at good as a GPS. The application source code is worth looking at in detail. He uses techniques that are more advanced than calling WebClient, and as a programmer it is interesting to know how to do same thing many different ways (viewing this link requires you have Silverlight 3 installed):

http://www.nikhilk.net/Content/Posts/SilverlightLocateMe/LocateMe.htm

This post is about integrating this functionality into the mapping application I am currently developing. I have decided to create a separate class of objects to mark my current location. I did this mainly because I wanted to use a different symbol to mark myself on the map -- like a "You Are Here" marker. I have extended this class to collect a time stamp, and this -- in combination with persistence -- can be used to track my movements.


public class MyLocation : PointOfInterest



{



public DateTime TimeStamp { get; set; }



public MyLocation()



: base()



{



TimeStamp = DateTime.Now;



}







#region Serializer Methods



public override XElement PersistTo(ModelSerializer oSerializer, XElement oSelf)



{



XElement oXElement = base.PersistTo(oSerializer, oSelf);







oXElement.Add(new XAttribute("TimeStamp", TimeStamp.ToString()));







return oXElement;



}



public override IMapModel RecoverFrom(ModelSerializer oSerializer, XElement oElement)



{



var oResult = base.RecoverFrom(oSerializer, oElement);







string sTimeStamp = oElement.Attribute("TimeStamp").Value;



TimeStamp = DateTime.Parse(sTimeStamp);



return oResult;



}



#endregion



}
I needed to modify the rendering code in order to display a different picture for this class. With this quick change, objects of type MyLocation now show a pushpin instead of a red rectangle.


void RenderPoint(PointOfInterest oPOI)



{







if (oPOI is MyLocation)



{



Uri oSource = new Uri(@"Pushpin.png", UriKind.Relative);



var oBitmap = new BitmapImage(oSource);



var oImage = new Image() { Source = oBitmap, Width=23, Height=27 };



m_oLayer.AddChild(oImage, oPOI.Loc, PositionMethod.BottomCenter);



}



else



{



// Create a Rectangle Shape to Show as the "Pushpin"



// Set it's Size to 10x10



var oRect = new System.Windows.Shapes.Rectangle() { Fill = new SolidColorBrush(Colors.Red), Width = 10, Height = 10 };







// Add Rectangle to MapLayer



m_oLayer.AddChild(oRect, oPOI.Loc, PositionMethod.Center);



}



}
The code to query the web service using WebClient follows. This is an asynchronous call that returns a string delimited by line feeds, so I used Nikhil's trick of reading from a stream to parse out and convert the Lat/Long information.

void WhereAmI()



{



WebClient oClient = new WebClient();



var oAddress = new Uri(@"http://api.hostip.info/get_html.php?position=true");



oClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(oClient_DownloadStringCompleted);



oClient.DownloadStringAsync(oAddress);



}







void oClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)



{



string sResult = e.Result;







using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(sResult))))



{



try



{



sr.ReadLine();



sr.ReadLine();







double lat = 0;



TryParse<Double>(sr.ReadLine(), out lat);







double lng = 0;



TryParse<Double>(sr.ReadLine(), out lng);







var oPOI = new MyLocation() { Loc = new Location(lat, lng) };



m_oList.Add(oPOI);







RenderPoint(oPOI);







}



catch { }



}



MessageBox.Show(sResult);



}


Monday, June 1, 2009

Using the Virtual Earth Silverlight Control - Part One

Debra and I took a trip around the country last month, 7500 miles of driving over 25 days. You can read about it at http://www.wiredwalkabout.com/.

One of the goals of the trip was to fieldtest technology, including the Virtual Earth Silverlight control. You can see the MIX09 presentation here at http://videos.visitmix.com/MIX09/T34F.

So you might be thinking, why not just do this from home? I believe that you can see the issues more clearly if you put yourself into the situations in which you expect the software to be used. After all, Pixar animators traveled to South America as inspiration for the movie "UP", and traveled along Route 66 in preparation for the movie "Cars".

Along the way we were using a Garmin to plan our trip. This is an extremely valuable tool, but it was missing some features that would have been nice to have, so I decided to investigate the Virtual Earth Silverlight control to see what it would take to reproduce/extend the features in the Garmin. I have also done Telco applications in the past and I wanted to investigate how the mapping requirement in these applications could be accomplished in Silverlight.

video

Since this investigation was going to be more like building an application and less like an experiment, I am starting by writing code to persist my POCO domain model (Plain Old CLR Objects). I am using the OpenFileSave feature in Silverlight 3 to save my domain model as XML. The rest of this blog entry explains my model for persistence.

For this application I am implementing my own XML serializer. The result (so far) is as follows. As the implementation grows I expect this file format to change. XML gives me some flexibility in reusing data from older files.


<?xml version="1.0" encoding="utf-8"?>
<ModelSerializer>
<PointsOfInterest>
<PointOfInterest Latitude="39.730288305639185" Longitude="-83.310517678613749" />
<PointOfInterest Latitude="34.004710072445405" Longitude="-89.814423928613749" />
<PointOfInterest Latitude="32.572126585547366" Longitude="-96.669892678613763" />
<PointOfInterest Latitude="30.436679980300028" Longitude="-98.032197366113763" />
<PointOfInterest Latitude="31.304215774927229" Longitude="-103.74508799111376" />
<PointOfInterest Latitude="33.272989725895094" Longitude="-111.96286142861376" />
<PointOfInterest Latitude="36.445446009018042" Longitude="-118.24704111611376" />
<PointOfInterest Latitude="38.845986120977358" Longitude="-120.35641611611376" />
<PointOfInterest Latitude="37.463816739355629" Longitude="-122.11422861611376" />
<PointOfInterest Latitude="41.82646221909387" Longitude="-121.71872080361376" />
<PointOfInterest Latitude="45.511996049055" Longitude="-122.24606455361376" />
<PointOfInterest Latitude="47.478111391249712" Longitude="-122.20211924111376" />
<PointOfInterest Latitude="46.3676556511152" Longitude="-117.06051767861376" />
<PointOfInterest Latitude="45.604305565410648" Longitude="-110.68844736611376" />
<PointOfInterest Latitude="44.07759180028043" Longitude="-103.52536142861376" />
<PointOfInterest Latitude="43.410903675485677" Longitude="-96.582002053613763" />
<PointOfInterest Latitude="41.924626769221476" Longitude="-89.902314553613749" />
</PointsOfInterest>
</ModelSerializer>



The PointsOfInterest and PointOfInterest classes both implement the PersistTo XML and RecoverFrom XML methods defined in the IMapModel interface. Here is the code:



public interface IMapModel
{
XElement PersistTo(ModelSerializer oSerializer, XElement oSelf);
IMapModel RecoverFrom(ModelSerializer oSerializer, XElement oElement);
}

public class PointsOfInterest : List<PointOfInterest>, IMapModel
{
#region Serializer Methods
public virtual XElement PersistTo(ModelSerializer oSerializer, XElement oSelf)
{
XElement oXElement = new XElement(GetType().Name);
oSelf.Add(oXElement);

foreach (IMapModel oIMap in this)
oIMap.PersistTo(oSerializer, oXElement);

return oSelf;
}
public virtual IMapModel RecoverFrom(ModelSerializer oSerializer, XElement oElement)
{
string sType = GetType().Name;
XElement oRefElement = oElement.Element(sType);
if (oElement.HasElements)
foreach (XElement oSubElement in oRefElement.Elements())
{
IMapModel oChild = oSerializer.Import(this, oSubElement);

PointOfInterest oPOI = oChild as PointOfInterest;
if (oPOI != null)
this.Add(oPOI);
}

return this;
}
#endregion
}

public class PointOfInterest : IMapModel
{
public Location Loc { get; set; }

#region Serializer Methods
public virtual XElement PersistTo(ModelSerializer oSerializer, XElement oSelf)
{
XElement oXElement = new XElement(GetType().Name);
oSelf.Add(oXElement);

oXElement.Add(new XAttribute("Latitude",Loc.Latitude));
oXElement.Add(new XAttribute("Longitude", Loc.Longitude));

return oXElement;
}
public virtual IMapModel RecoverFrom(ModelSerializer oSerializer, XElement oElement)
{
Loc = new Location(double.Parse(oElement.Attribute("Latitude").Value), double.Parse(oElement.Attribute("Longitude").Value));
return this;
}
#endregion
}


This is not the first time I have implemented my own serializer. Most of my applications implement a highly interrelated domain model, combining objects sourced from many data sources into a domain model with many pointers and references. I have found the additional control I get over the serialization implementation to outweigh the effort of implementation. However, before this series of posts is finished, I plan on investigating the Silverlight 3 tools for persistence in XAML, XML and JSON.
Here is the code for ModelSerializer and the save and restore methods that start the process.


public class ModelSerializer
{

#region Compute Type

public Type ComputeType(XElement oElement)
{
string sType = oElement.Name.ToString();
return ComputeType(sType);
}

public Type ComputeType(object oObject)
{
if (oObject.GetType().IsSubclassOf(typeof(Type)))
return ComputeType(oObject as Type);
else
return ComputeType(oObject.ToString());
}

private Dictionary<String, Type> m_oTDictionary = null;
public Dictionary<String, Type> TypeHash
{
get
{
if (m_oTDictionary == null)
{
m_oTDictionary = new Dictionary<String, Type>();
m_oTDictionary.Add("double", typeof(Double));
m_oTDictionary.Add("string", typeof(String));
m_oTDictionary.Add("boolean", typeof(Boolean));
m_oTDictionary.Add("bool", typeof(Boolean));
m_oTDictionary.Add("integer", typeof(Int32));
m_oTDictionary.Add("int32", typeof(Int32));
m_oTDictionary.Add("object", typeof(object));
}
return m_oTDictionary;
}
set
{
m_oTDictionary = value;
}
}
#endregion


private XDocument m_oDocument = null;
public XDocument Document
{
get
{
if (m_oDocument == null)
m_oDocument = new XDocument();

return m_oDocument;
}
set
{
m_oDocument = value;
}
}

public void Write(StreamWriter oStream)
{
Document.Save(oStream);
}

public XDocument Read(StreamReader oStream)
{
Document = XDocument.Parse(oStream.ReadToEnd());
return Document;
}


public virtual IMapModel CreateObject(XElement oElement)
{
Type oType = ComputeType(oElement);
try
{
return Activator.CreateInstance(oType) as IMapModel;
}
catch (Exception ex)
{
oType = ComputeType(oElement);
string sType = oElement.Name.ToString();
string sMessage = string.Format("Type {0} not created. {1}", sType, ex.Message);
MessageBox.Show(sMessage);
}
return null;
}

public XElement Export(IMapModel oObject, XElement oParent)
{
return SerializeObject(oObject, oParent);
}


public IMapModel Import(IMapModel oObject, XElement oElement)
{
IMapModel oResult = CreateObject(oElement);
if (oResult != null)
DeSerializeObject(oResult, oElement);

return oResult;
}

public virtual void Serialize(IMapModel oObject)
{
Serialize(oObject, Document);
}
public virtual void Serialize(IMapModel oObject, XDocument oDocument)
{
XElement oElement = new XElement(GetType().Name);
oDocument.Add(oElement);
SerializeObject(oObject, oElement);
}


public virtual IMapModel DeSerialize(IMapModel oObject)
{
IMapModel oResult = DeSerialize(Document, oObject);
return oResult;
}

public virtual IMapModel DeSerialize(XDocument oDocument, IMapModel oTarget)
{
XElement oRoot = oDocument.Root;
IMapModel oResult = DeSerializeObject(oTarget, oRoot);
return oResult;
}

public virtual XElement SerializeObject(IMapModel oObject, XElement oElement)
{
return oObject.PersistTo(this, oElement);
}

public virtual IMapModel DeSerializeObject(IMapModel oObject, XElement oElement)
{
oObject.RecoverFrom(this, oElement);
return oObject;
}

}



void btnSave_Click(object sender, RoutedEventArgs e)
{

Button oButton = sender as Button;

SaveFileDialog sfd = new SaveFileDialog()
{
DefaultExt = "xml",
Filter = "XML files (*.xml)*.xmlAll files (*.*)*.*",
FilterIndex = 1
};

bool? result = sfd.ShowDialog();
if (result == true)
{
try
{
using (StreamWriter oStream = new StreamWriter(sfd.OpenFile()))
{
ModelSerializer oSerializer = new ModelSerializer();
oSerializer.Serialize(m_oList);
oSerializer.Write(oStream);
oStream.Close();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}

void btnLoad_Click(object sender, RoutedEventArgs e)
{
Button oButton = sender as Button;

OpenFileDialog ofd = new OpenFileDialog()
{
Filter = "XML files (*.xml)*.xmlAll files (*.*)*.*",
FilterIndex = 1
};

bool? result = ofd.ShowDialog();
if (result == true)
{
try
{
using (StreamReader oStream = ofd.File.OpenText())
{
ModelSerializer oSerializer = new ModelSerializer();
oSerializer.Read(oStream);
oSerializer.DeSerialize(m_oList);
oStream.Close();
}

foreach (PointOfInterest oPOI in m_oList)
RenderPoint(oPOI);

}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}


All the source code is on Codeplex:

http://vearthmappingexample.codeplex.com/