Virtual Earth and Microsoft Commerce Server
If you've visited us at the Microsoft booth at the Shop.org conference, you may have seen the Microsoft Virtual Earth demonstration application illustrating Microsoft Commerce Server 2007 and Virtual Earth working together. The concept for the application is based on inventory data in Commerce Server being used to help end users find the nearest location that carries the item you're searching for - an advanced store locator and inventory search scenario.
The demonstration application is posted online branded as "Soundshift Multi-Channel Retailing." To use the application, you'll want to go to the "stores" tab and enter '90210' in the address box. You'll be presented with the results around the Los Angeles area inclusive of custom pushpins, custom EROs and how 'bout that custom navigation bar? Nice. In the "Directions" tab you can get directions, find gas stations along the route, find coffee shops along the route, show traffic, print the directions, send to email, and send to mobile. A pretty robust application, eh?
So, to most of you this is just a plain old store location - whoopie! The important piece is what happens behind the scenes. The folks at Cactus Commerce built the implementation and wrote a white paper describing how to leverage both Windows Live ID and Virtual Earth within Commerce Server. The document provides architecture and code details of how this can be done using the Commerce Server's extensibility model. From the document, you'll find the architecture and code level instructions for integrating the following features into Commerce Server - geocoding a user's address, finding the nearest stores, retrieving items in the basket, getting inventory status, and adding store and inventory information to the Virtual Earth map.
The first step in locating nearby stores is to geocode the user’s address to latitude and longitude coordinates. You can use the Find method from the Virtual Earth’s Map Control to resolve the address. For simplicity, we will assume only one result is returned:
function findAddress(address, map)
{
// Call VEMap.Find() to geocode address
map.Find(null, address, null, null, null, null, false, false, true, true,
findAddressCallBack);
}
// Call back function for VEMap.Find()
function findAddressCallBack(thelayer, resultsArray, places, hasMore, veErrorMessage)
{
if(places != null && places.length >0)
{
var latitude = places[0].LatLong.Latitude;
var longitude = places[0].LatLong.Longitude;
}
}
Once we have the user’s geocode, we can implement the logic to find nearby stores within a given radius:
private void GetNearbyStores(ProfileManagementContext profileCtx, double latitude,
double longitude, double radius, List<Store> stores)
{
// Retrieve store list
DataSet dsEntities = profileCtx.GetSearchableEntities();
SearchClauseFactory scf = profileCtx.GetSearchClauseFactory(dsEntities, "Store");
SearchClause searchClause = scf.CreateClause();
Microsoft.CommerceServer.SearchOptions searchOptions =
new Microsoft.CommerceServer.SearchOptions();
searchOptions.PropertiesToReturn =
"u_store_id,u_name,u_address,u_virtual_catalog, latitud,longitude";
DataSet ds = profileCtx.ExecuteSearch("StoreObject", searchClause, searchOptions);
// Find nearby stores
if (ds != null && ds.Tables.Count > 0)
{
foreach (DataRow r in ds.Tables[0].Rows)
{
double storeLat = double.Parse(r["latitud"].ToString());
double storeLon = double.Parse(r["longitude"].ToString());
// CalculateDistance() implements the Mercator's Projection
// to determine the distance between 2 points
if (CalculateDistance(lat, lon, storeLat, storeLon) < radius)
{
// Store is within the specified radius
Store store = new Store();
store.ID = r["u_store_id"].ToString();
store.Name = r["u_name"].ToString();
store.Address = r["u_address"].ToString();
store.VirtualCatalog = r["u_virtual_catalog"].ToString();
store.Latitude = storeLat;
store.Longitude = storeLon;
stores.Add(store);
}
}
}
}
In order to build up the store stock status return data set, we need to retrieve the list of products from the user’s basket:
// Get list of product and quantity from user's basket
private void GetBasketItems(GUID userID, string basketName, List<BasketItem> basketItems)
{
// Get the user's shopping cart
Basket basket = CommerceContext.Current.OrderSystem.GetBasket(userID, basketName);
foreach (OrderForm orderForm in basket.OrderForms)
{
foreach (LineItem lineItem in orderForm.LineItems)
{
// Store basket item
BasketItem basketItem = new BasketItem();
basketItem.ProductID = lineItem.ProductId.ToString();
basketItem.ProductName = lineItem.DisplayName.ToString();
basketItem.Quantity = lineItem.Quantity;
basketItems.Add(basketItem);
}
}
}
We are now ready to retrieve on hand quantity for each store items:
private void GetStoreInventoryStatus(InventoryContext inventoryCtx, List<Store> stores,
List<BasketItem> basketItems)
{
foreach (Store store in stores)
{
// Get the inventory catalog for the current store
InventoryCatalog inventoryCatalog =
inventoryCtx.GetAssociatedInventoryCatalog(store.VirtualCatalog);
foreach (BasketItem basketItem in basketItems)
{
// Get Inventory Sku
InventorySku inventorySku =
inventoryCatalog.GetSku(store.VirtualCatalog,
basketItem.ProductID);
// Add inventory status to store
ItemStatus itemStatus = new ItemStatus();
itemStatus.ProductID = basketItem.ProductID;
itemStatus.StockStatus = (inventorySku.Quantity - basketItem.Quantity) >= 0 ?
"In Stock" : "Not Available";
store.ItemStatusList.Add(itemStatus);
}
}
}
Now that we have all the Store inventory data, we will be adding pushpins and popup descriptions to the Virtual Earth Map via client side scripting:
· Create a new instance of the map control and add a shape layer:
map = new VEMap('myMap');
map.LoadMap();
layer = new VEShapeLayer();
map.AddShapeLayer(layer);
layer.Hide();
· Create pushpin shape and add the shape layer
// jsStoreStockData contains store inventory status retrived from
// previous steps
for (var i=0; i < jsStoreStockData.stores.length; i++)
{
// Create shape (pushpin)
var latLong = new VELatLong(jsStoreStockData.stores[i].Latitude,
jsStoreStockData.stores[i].Longitude) ;
var shape = new VEShape(VEShapeType.Pushpin, latLong);
shape.SetTitle('<H3>'+jsStoreStockData.stores[i].Name+'</H3>');
shape.SetCustomIcon("<img width='20' height='20' alt='store'
src='images/logo_vista.png'/>");
// Generate basket item stock status as shape description
var desc = '<div>'+jsStoreStockData.stores[i].Address+'<table>' ;
desc += '<tr><td><b>Product</b></td><td><b>Stock Status<td></td></tr>'
for (var j = 0; k < jsStoreStockData.stores[i].ItemStatusList.length; j++)
{
desc += '<tr><td>'+jsStoreStockData.stores[i].ItemStatusList[j].ProductName
+ '</td><td>';
desc += jsStoreStockData.stores[i].ItemStatusList[j].StockStatus
+ '</td></tr>';
}
desc += '</table></div>';
shape.SetDescription(desc);
// Add pushpin to ShapeLayer
layer.AddShape(shape);
// Center map and set zoom level
SetCenterAndZoom();
// Show the ShapeLayer
layer.Show();
}
If you have Microsoft Commerce Server 2007, your integration with Microsoft Virtual Earth just got a heck of a lot easier. The flexibility of the Virtual Earth platform allows for data visualization from almost any data repository whether it be Microsoft products like Commerce Server, CRM or SQL Server or non-Microsoft data sources such as Siebel, Oracle or MySQL. The proliferation of data visualization using mapping across the web and software products just keeps getting better, doesn't it?
CP