Friday, May 29, 2009

Microsoft Virtual Earth Tutorial

What is Virtual Earth?

Microsoft Virtual Earth (rebranded as Bing Map in middle of 2009) is a platform to visualize location based data over a simple web connection, and is a powerful service that renders interactive maps directly in web pages. By combining the map images provided by Microsoft’s platform with our custom data, we can create an interactive experience for end users to do the location-related searches.

A Simple Map Page

Virtual Earth is rendered purely by JavaScript in client side browser. The latest version of VE JavaScript library is 6.2. Loading a VE Map is simple: reference the VE JavaScript library, create a VEMap object and call loadMap method to show the map. The source of a simple web page and its UI screen-shoot below demos the simplicity of VE Map usage.
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
<script type="text/javascript">
var map = null;
function LoadMap() {
map = new VEMap('myMap');
map.LoadMap();
}
</script>
</head>
<body onload="LoadMap();">
<form id="form1" runat="server">
<div id='myMap' style="position: relative; width: 600px; height: 400px;"></div>
</form>
</body>
</html>
The page looks like:


Map Initial Setting and Show Map Info

VE map initial location and scale can be set in the loading. VELatLong object is passed to LoadMap method to locate the central point of the map, and view level or zoom level can be set from 0 – 19 (default 4 showing the most part United States). To get the map geo info, we use MapView object, and the minimum and maximum latitude/longitude values of the map can be read by map’s corner points. Following page source illustrates how to initialize a map centering at Microsoft with scale of city view, and how to get map information.
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
<script type="text/javascript">
var map = null;
function LoadMap() {
map = new VEMap('myMap');
map.LoadMap(new VELatLong(47.65, -122.14), 12);
showMapInfo();
}
function showMapInfo() {
var view = map.GetMapView();
latMin = view.BottomRightLatLong.Latitude;
latMax = view.TopLeftLatLong.Latitude;
lonMin = view.TopLeftLatLong.Longitude;
lonMax = view.BottomRightLatLong.Longitude;
latCenter = map.GetCenter().Latitude;
lonCenter = map.GetCenter().Longitude;
var mapInfo = 'Map Info - Min lat: '+ latMin.toFixed(6);
mapInfo += ' Max lat:' + latMax.toFixed(6);
mapInfo += ' Min lon: ' + lonMin.toFixed(6);
mapInfo += ' Max lon: ' + lonMax.toFixed(6);
mapInfo += '<br> Center Lat: ' + latCenter.toFixed(6);
mapInfo += ' Center lon: ' + lonCenter.toFixed(6);
mapInfo += ' Zoom Level: ' + map.GetZoomLevel();
document.getElementById('divMapInfo').innerHTML = mapInfo;
}
</script>
</head>
<body onload="LoadMap();">
<form id="form1" runat="server">
<div id="divMapInfo" style="height: 50px;"></div>
<div id='myMap' style="position: relative; width: 600px; height: 400px;"></div>
</form>
</body>
</html>
The page is loaded as:


Capture Map Events

All user interactive actions in the map pages are triggered by events and handled by event handlers. There are more than ten events available on VE maps. To handle a map event, you have to attach it explicitly to an event handler (JavaScript function) by calling map.AttachEvent(eventName, eventHandler). The details of VE Map events are listed in http://msdn.microsoft.com/en-us/library/bb429568.aspx, such as onendzoom event occurring when the map zoom ends and onresize event happens in resizing. Besides the events described in the above link, common mouse-based events, like onclick, ondoubleclick and onmouseover events, can also be attached to a map.

The example map page above only shows the map information of the initial state. To see the updated info when there is a change on the map, we can use the onchangeview event:
       function LoadMap() {
map = new VEMap('myMap');
map.LoadMap(new VELatLong(47.65, -122.14), 12);
map.AttachEvent("onchangeview", showMapInfo);
showMapInfo();
}

Pushpins on the Map

Pushpins are visible locaters on the map. Pushpins can be used to show location specific landmarks or custom data. The pushpin is abstracted in a VE Shape object, and the Shape object model allows users to customize the UI shown on the map. With common DHTML techniques, we can substitute images, controls and dynamic layers for the default pushpin control.

Following code shows how to create a pushpin with image and description:
function LoadMap() {
map = new VEMap('myMap');
map.LoadMap(new VELatLong(47.65, -122.14), 12);
var imageURL = 'http://www.microsoft.com/about/images/link_visitMS_new.gif';
addPushpin(47.65, -122.14, 'My pushpin', 'Just a test', imageURL);
showMapInfo();
}
function addPushpin(lat, lon, title, description, icon) {
var point = new VELatLong(lat, lon);
var shape = new VEShape(VEShapeType.Pushpin, point);
shape.SetTitle(title);
shape.SetDescription(description);
if (icon) {
shape.SetCustomIcon(icon);
}
map.AddShape(shape);
}
function showMapInfo() {
//Skip the rest for abbreviation...
Result:


Showing Dynamic Data Using AJAX and JSON Objects

In this example custom pushpins are populated on the map dynamically using AJAX call with JSON data transfer.jQuery is used to facilitate the AJAX invocation:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Dynamically Loading Virtual Map</title>

<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
<script type="text/javascript" src="http://www.json.org/json2.js"></script>
<script src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js" type="text/javascript"></script>

<script type="text/javascript">
var map = null;
function LoadMap() {
map = new VEMap('myMap');
map.LoadMap(new VELatLong(47.64, -122.13), 12);
map.SetScaleBarDistanceUnit(VEDistanceUnit.Kilometers);
map.AttachEvent("onchangeview", showMapInfo);
showMapInfo();
}

function showMapInfo() {
var view = map.GetMapView();
latMin = view.BottomRightLatLong.Latitude;
latMax = view.TopLeftLatLong.Latitude;
lonMin = view.TopLeftLatLong.Longitude;
lonMax = view.BottomRightLatLong.Longitude;
latCenter = map.GetCenter().Latitude;
lonCenter = map.GetCenter().Longitude;

var mapInfo = 'Map Info - ' + ' Min lat: ' + latMin.toFixed(6);
mapInfo += ' Max lat:' + latMax.toFixed(6);
mapInfo += ' Min lon: ' + lonMin.toFixed(6);
mapInfo += ' Max lon: ' + lonMax.toFixed(6);
mapInfo += '<br> Center Lat: ' + latCenter.toFixed(6);
mapInfo += ' Center lon: ' + lonCenter.toFixed(6);
mapInfo += ' Zoom Level: ' + map.GetZoomLevel();

$('#divMapInfo').html(mapInfo);

if (map.GetZoomLevel() > 15) {
getDataByLatLonArea(latMin, latMax, lonMin, lonMax);
}
else {
map.DeleteAllShapes();
}
}

function getDataByLatLonArea(latMin, latMax, lonMin, lonMax) {
var parameters = "{'latMin':" + latMin + ",'latMax':" + latMax;
parameters += ",'lonMin':" + lonMin + ",'lonMax':" + lonMax + "}";
$.ajax({
type: "POST",
url: "MapService.asmx/GetDataByGeoRange",
data: parameters,
contentType: "application/json; charset=utf-8",
dataType: "json",
error: function(xhr, desc, exceptionobj) {
alert(xhr.responseText);
},
success: function(msg) {
if (msg.d != null) {
var jsonObjects = JSON.parse(msg.d);
//var mapInfo = $('#divMapInfo').html();
//mapInfo += " <b>Item found:" + jsonObjects.length; $('#divMapInfo').html(mapInfo);
$.each(jsonObjects, function(i, item) {
var description = populateDescription(item);
addPushpin(item.Latitude, item.Longitude, item.Name, description, null);
});
}
}
});
}

function populateDescription(item) {
var description = "<br><b>Description for " + item.Name + "<br><br>";
return description;
}

function addPushpin(lat, lon, title, description, icon) {
var point = new VELatLong(lat, lon);
var shape = new VEShape(VEShapeType.Pushpin, point);
shape.SetTitle(title);
shape.SetDescription(description);
if (icon) {
shape.SetCustomIcon(icon);
}
map.AddShape(shape);
}
</script>

</head>
<body onload="LoadMap();">
<form id="form1" runat="server">
<div id="divMapInfo" style="height: 50px;">
</div>
<div id='myMap' style="position: relative; width: 600px; height: 400px;">
</div>
</form>
</body>
</html>
Note that we only show the pushin data when the zoom level is greater than 15, and won’t add any pushpins on the map when the scale of the map is too big.

The Web Service returns data in JSON format, which is generated by DataContractJsonSerializer class directly, a new feature in .NET 3.5:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
using System.Text;
using System.IO;

//From System.Web.Extensions assembly (.NET 3.5 SP1)
using System.Web.Script.Services;
//From System.ServiceModel.Web assembly (.NET 3.5 SP1)
using System.Runtime.Serialization.Json;

namespace Web
{
/// <summary>
/// Summary description for MapService
/// </summary>
[ScriptService]
[WebService(Namespace = "http://tempuri.org/")]
public class MapService : System.Web.Services.WebService
{
[WebMethod]
public string GetDataByGeoRange(double latMin, double latMax, double lonMin, double lonMax)
{
List<MapData> dataList = MapData.GetDataByGeoRange(latMin, latMax, lonMin, lonMax);
DataContractJsonSerializer serializer =
new DataContractJsonSerializer(dataList.GetType());
StringBuilder sb = new StringBuilder();
using (MemoryStream ms = new MemoryStream())
{
serializer.WriteObject(ms, dataList);
sb.Append(Encoding.UTF8.GetString(ms.ToArray()));
}
return sb.ToString();
}
}

public class MapData
{
private struct Point
{
public string Name;
public double Latitude;
public double Longitude;
public Point(string name, double lat, double lon)
{
Name = name;
Latitude = lat;
Longitude = lon;
}
}

public string Name { get; set; }
public string Latitude { get; set; }
public string Longitude { get; set; }

static private List<Point> mockData = GetMockData();
static private List<Point> GetMockData()
{
int pointNumber = 1000;
double latMin = 47.593662, latMax = 47.686192;
double lonMin = -122.232857, lonMax = -122.026863;
List<Point> mockPoints = new List<Point>();
Random rand = new Random();
for (int i = 0; i < pointNumber; i++)
{
double lat = rand.NextDouble() * (latMax - latMin) + latMin;
double lon = rand.NextDouble() * (lonMax - lonMin) + lonMin;
Point point = new Point("Mock " + i.ToString(), lat, lon);
mockPoints.Add(point);
}
return mockPoints;
}
public static List<MapData> GetDataByGeoRange(double latMin, double latMax, double lonMin, double lonMax)
{
List<MapData> dataList = new List<MapData>();
foreach (Point point in mockData)
{
if (point.Latitude > latMin && point.Latitude < latMax &&
point.Longitude > lonMin && point.Longitude < lonMax)
{
MapData mapData = new MapData();
mapData.Latitude = string.Format("{0:0.000000}", point.Latitude);
mapData.Longitude = string.Format("{0:0.000000}", point.Longitude);
mapData.Name = point.Name;
dataList.Add(mapData);
}
}
return dataList;
}
}
}
The screen shot of the map with dynamically loaded data:


Conclusion

In this article we examine a few simple scenarios of using Microsoft Virtual Earth. In reality we can build very powerful location-based web applications using Microsoft Virtual Earth as a platform.

Tuesday, May 12, 2009

SharePoint CKS - User Group Edition 1.0 Solution Package

The Toronto SharePoint User Group website (http://www.tspug.com/) needs to rebuild for some considerations. Bill Brockbank, a SharePoint MVP, has been preparing on this for a while. He suggested to use Community Kit for SharePoint (CKS) as template to start our work. What we are using is the User Group Edition, a SharePoint stp template. There's some limitation in stp template in terms of development and deployment. So we decided to convert the stp to a solution package with Site Definition.

Creating a SharePoint Site Definition from scratch is not a trivial task for me. I spent quite a bit of time writing all those XML files including the ONET.XML. It was a good exercise and I got more understanding of what's under the hood of building a new SharePoint site. With the solution conversion completed, our new user group website will be setup online soon.

The whole solution package can be download here.