The Making of the Virtual Earth 3D Santa Tracker

*** Updated 2023 ***
Please see our latest Santa Tracker Blog at HO HO HO! Microsoft and Bing Maps help NORAD track Santa! | Maps Blog

As Santa heads north for his annual slumber and we’re all left with buyer’s remorse and daunting credit card bills we can now reflect on something positive: how Christmas was changed this year for the better with a 3D version of tracking Santa using Microsoft Virtual Earth. And, hey, I was stoked – I got my application featured on MSN.com, so Merry Christmas to me!

Bing Maps Santa Tracker

It started with The North Pole – A Virtual Earth Christmas Experience. Now, The North Pole Experience wasn’t that hard – Tom [Grimes] (and his Caligari friends) built the models, I crunched the map tile overlays and then wrote the code to pull everything together. But, tracking Santa was way more difficult because of the time constraints as to when Santa began his flight and when to end it, not to mention all of the coding behind ripping apart the GeoRSS file – well worth sharing. I’ll dig into this and actually provide you with my code (code junkies may skip to the end).

I thought the biggest challenge for me was going to be figuring out when to start Santa flying and when to end him, but it turned out the parsing of a GeoRSS file for customization was more of a challenge. However, I used quite a bit of Virtual Earth functionality, so I figured this was just as good a place to document some of it as any. We used raster tile overlays, 3D model importation and GeoRSS importation and customization.

Raster Tile Overlays

Like many things in Virtual Earth it’s not terribly difficult to overlay raster images over any area in Virtual Earth. So, I grabbed some icy Antarctica imagery (courtesy of LIMA), cut it into an island shape and dropped it at the magnetic North Pole using the AddTileLayer() Method. The actual North Pole is somewhat of a black hole in VE3D that we shade the color of the ocean – so, don’t try to overlay tiles or lines there as it won’t work. The code for importing my tiles was as follows (the entire solution is at the end of the post). Note that I set the NumServers property because of the load balancing in MSNBC’s farm. Also, I set the ZIndex to a super high number just because I felt like it. I could’ve set it to 1 or 2 and it would’ve worked fine. I set the MinZoom and MaxZoom because the tiles that I created (for zoom levels 1-10) were actually getting called and putting a burden on the already heavy download of VE3D and the 3D models. Doing it this way only rendered the Level 10 tiles, but at all 23 zoom levels (only 19 zoom levels are supported in the control, natively).

function GetTiles()
{
    // Names layer for tiles and specifies location
    var tileSourceSpec = new VETileSourceSpecification(“NorthPoleIslandTiles”, “
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/tiles/%4.png”);
    tileSourceSpec.NumServers = 100;
    tileSourceSpec.ZIndex = 100;
    tileSourceSpec.MinZoom = 10;
    tileSourceSpec.MaxZoom = 23;
    try{
        map.AddTileLayer(tileSourceSpec, true);
    }
    catch (e) {
        alert(e.message);
    }
}

3D Models

Importing the 3D models was also very straightforward using Import3DModel() Method. The real brilliance was the creativity in imagining up what the North Pole looks like, what entities to create and how to lay it all out. The Toy Shop, the carousel, the polar bears, etc. Plus, Santa and Mrs. Claus – what should they look like, etc. All of this was done by Tom and the Caligari Community crew of Augusto Michelis, Heidi Simonsen, Matthew Collins and Stephen May (we can’t thank them enough). We consolidated some of the models for ease and control of placement. Why did they look so realistic? Much of that was a result of the high polygon count. Plus, the textures downloaded simultaneous with the models. So, we sacrificed a bit of bandwidth for high resolution models. Here’s the code I used for Santa’s sleigh in real time. Note the variable llCurrentPosition. This was dynamic since Santa would change position every 7.05 minutes. More of why that is in the GeoRSS Parsing section below.

function loadSanta(latlong) {
    //Set HTTP location for sleigh model
    var sourceSleighDirectory = “
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/objs/FullSleigh4/”;

    var llCurrentPosition = new VELatLong();
    llCurrentPosition = latlong;
    llCurrentPosition.AltitudeMode = VEAltitudeMode.Absolute;

    //Specifies sleigh model attributes
    var modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceSleighDirectory + “fullsleigh_1.obj”, NorthPoleLayer);
    var sleighOrientation = new VEModelOrientation(0, 0, 0);

    // Load the model into the 3D map instance (model source, callback, latlong, orient, scale)
    try {
        map.Import3DModel(modelSpec, onSantaLoad, llCurrentPosition, sleighOrientation, null);
    }
    catch (e) {
        alert(e.message);
    }
    //Center Map on current position
    try {
        map.SetCenter(llCurrentPosition);
    }
    catch (e) {
        alert(e.message);
    }
}

GeoRSS Parsing

Importing a GeoRSS file into Virtual Earth is simple. You simply use the ImportShapeLayerData() Method which natively imports file types such as GeoRSS, GPX and KML. Well, I didn’t want to render the data I wanted to query an entity in the XML file to identify where Santa was at any given time. This was no easy task, so I hope some of this helps anyone else who is trying to push Virtual Earth’s limits.

In the ImportShapeLayerData() Method there are 3 arguments (a link to the XML file, the call back function after the file is loaded, and whether or not to set the best zoom level given the points).

function LoadSantaGeoRSS() {
    var santaShapeSourceSpec = new VEShapeSourceSpecification(VEDataType.ImportXML, ‘
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/TrackingSanta.xml’, santaRouteLayer);
    santaShapeSourceSpec.MaxImportedShapes = 300;
    try {
        map.ImportShapeLayerData(santaShapeSourceSpec, onGeoRSSLoad, 0);
    }
    catch (e) {
        alert(e.message);
    }
}

Now, the tricky part was getting information out using the call back so I could manipulate it. The XML file I had contained many points and meta data information, but the Virtual Earth API will only return the Lat/Lons. Well, for tracking Santa that was perfect, but there were a few features I never got to finish – customizing icons, having callouts for where Santa was and tracing his route on the map. I get everything to work, but the callouts (but I didn’t deploy all of the features because I didn’t have time to test them. Tracing the route on the map was easy once I got the lat/lons out of the GeoRSS file and jammed into an array (AddShape()). So, all I had to do was divide the number of minutes he was in the sky by the number of stops to determine how frequently he should change position to make use of the entire file.

//After loading the GeoRSS file, extract the points.
var arrPoints = new Array();
function onGeoRSSLoad(layer) {
    layer.Hide();
    layer.SetTitle(“Santa’s Trek”);
    layer.SetDescription(“Santa’s Journey Around the World”);

    //Loop through points to draw line
    var intGeoRSSCount = layer.GetShapeCount();
    //Put GeoRSS into an array
    arrPoints.push(new VELatLong(84.6687, -154.1864));
    for (i = 0; i < intGeoRSSCount; i++) {
        arrPoints.push(layer.GetShapeByIndex(i).GetPoints()[0]);
     }
    arrPoints.push(new VELatLong(84.6687, -154.1864));

//…continued below in Time Zone section

Once I extracted the points, I just needed to select which point to put Santa at given the time of day (or night).

Time Zone

I wanted to start Santa flying at 8PM and end at 7AM. That seemed about right. Now, I had to take into consideration the fact that time zones are a killer when you want to have him in a particular location given the local time zone. So, no matter where you were at 8PM local time he would be in the same place. Uh, yeah, that was impossible to do in 2 days (oh yeah, I did this in 2 days), so I simplified and went logical by tracking one Santa (duh) and Coordinated Universal Time (UTC, aka Greenwich Mean Time (GMT)).

Next, I had to figure out what time is it in Greenwich when it’s 8PM on Dec. 24 in Tonga (the first time zone) when he takes off and then what time it is in Greenwich when it’s 6AM on Dec. 25 in Samoa (the last time zone) when he’s done. I went through a lot of paper and would’ve eaten a box of Lifebuoy Soap had my mother been present, but simplifying to a single Santa no what time it was where YOU are and using native JavaScript methods for calculating GMT made it simple. I didn’t take into account the month or year (why should I for a 24 hour adventure?) and only checked for days. If Santa wasn’t in the sky he was at The North Pole.

//Determine the time
var intUTC = new Date();
var intUTCDate = intUTC.getUTCDate();
var intUTCHour = intUTC.getUTCHours();
var intUTCMinute = intUTC.getUTCMinutes();
var intConvertedMinutes = (intUTCHour * 60) + intUTCMinute;
var currentArrayElement = Math.round((intConvertedMinutes / 7.05));

//Fetch the sleigh model given the time and location of Santa
//If he’s not flying he’s at the North Pole
if (intUTCDate == 24 & intUTCHour > 7) {
    loadSanta(arrPoints[currentArrayElement]);
}
else if (intUTCDate == 25 & intUTCHour < 17) {
    loadSanta(arrPoints[currentArrayElement]);
}
else {
    loadSanta(arrPoints[0]);
}

And, the rest is history. Oh, one method that is WAY underused is Dispose(). VE3D actually crashed my machine twice because I kept hitting refresh to check my application. Well, you can do a bit of garbage collection (read that as memory re-collection) by sticking the Dispose() Method in the BODY onunload event. My fan was screaming!

So, here’s the code in case you want to do something similar. Hey, it’s all love for Virtual Earth, so I’m happy if you use any of my code!

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html>
   <head>
      <title>You Better Watch Out, You Better Not Cry, You Better Not Pout, I’m Telling You Why, Santa Claus Is Coming To Town!</title>
      <meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″/>
      <script type=’text/javascript’ src=’
http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2′></script>
      <script type=”text/javascript”>

         var map = null;
         var NorthPoleLayer;
         var santaRouteLayer = new VEShapeLayer();
         var giftImageURL = “
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/objs/FullSleigh4/giftbox.png”;

         function GetMap()
         {
            map = new VEMap(‘myMap’);
            try {
                map.LoadMap();
            }
            catch(e)
            {
                alert(e.message);
            }
            try {
                map.SetMapStyle(VEMapStyle.Hybrid);
            }
            catch (e) {
                alert(e.message);
            }

            try {
                map.SetMapMode(VEMapMode.Mode3D);
            }
            catch (e) {
                alert(e.message);
            }
            try {
                map.onLoadMap = AddModel();
            }
            catch (e) {
                alert(e.message);
            }
        }  
         function GetTiles()
         {
             // Names layer for tiles and specifies location
             var tileSourceSpec = new VETileSourceSpecification(“NorthPoleIslandTiles”, “
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/tiles/%4.png”);
             tileSourceSpec.NumServers = 100;
             tileSourceSpec.ZIndex = 100;
             tileSourceSpec.MinZoom = 10;
             tileSourceSpec.MaxZoom = 23;
            try{
                 map.AddTileLayer(tileSourceSpec, true);
            }
            catch (e) {
                alert(e.message);
            }
        }

        function LoadSantaGeoRSS() {
            var santaShapeSourceSpec = new VEShapeSourceSpecification(VEDataType.ImportXML, ‘
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/TrackingSanta.xml’, santaRouteLayer);
            santaShapeSourceSpec.MaxImportedShapes = 300;
            try {
                map.ImportShapeLayerData(santaShapeSourceSpec, onGeoRSSLoad, 0);
            }
            catch (e) {
                alert(e.message);
            }
        }

        //After loading the GeoRSS file, extract the points.
        var arrPoints = new Array();
        function onGeoRSSLoad(layer) {
            layer.Hide();
            layer.SetTitle(“Santa’s Trek”);
            layer.SetDescription(“Santa’s Journey Around the World”);

            //Loop through points to draw line
            var intGeoRSSCount = layer.GetShapeCount();
            //Put GeoRSS into an array
            arrPoints.push(new VELatLong(84.6687, -154.1864));
            for (i = 0; i < intGeoRSSCount; i++) {
                arrPoints.push(layer.GetShapeByIndex(i).GetPoints()[0]);
             }
            arrPoints.push(new VELatLong(84.6687, -154.1864));

            //Determine the time
            var intUTC = new Date();
            var intUTCDate = intUTC.getUTCDate();
            var intUTCHour = intUTC.getUTCHours();
            var intUTCMinute = intUTC.getUTCMinutes();
            var intConvertedMinutes = (intUTCHour * 60) + intUTCMinute;
            var currentArrayElement = Math.round((intConvertedMinutes / 7.05));

            //Fetch the sleigh model given the time and location of Santa
            //If he’s not flying he’s at the North Pole
            if (intUTCDate == 24 & intUTCHour > 7) {
                loadSanta(arrPoints[currentArrayElement]);
            }
            else if (intUTCDate == 25 & intUTCHour < 17) {
                loadSanta(arrPoints[currentArrayElement]);
            }
            else {
                loadSanta(arrPoints[227]);
            }
        }

        function loadSanta(latlong) {
            //Set HTTP location for sleigh model
            var sourceSleighDirectory = “
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/objs/FullSleigh4/”;

            var llCurrentPosition = new VELatLong();
            llCurrentPosition = latlong;
            llCurrentPosition.AltitudeMode = VEAltitudeMode.Absolute;

            //Specifies sleigh model attributes
            var modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceSleighDirectory + “fullsleigh_1.obj”, NorthPoleLayer);
            var sleighOrientation = new VEModelOrientation(0, 0, 0);

            // Load the model into the 3D map instance (model source, callback, latlong, orient, scale)
            try {
                map.Import3DModel(modelSpec, onSantaLoad, llCurrentPosition, sleighOrientation, null);
            }
            catch (e) {
                alert(e.message);
            }
            //Center Map on current position
            try {
                map.SetCenter(llCurrentPosition);
            }
            catch (e) {
                alert(e.message);
            }
        }

        function trackSantasPath() {
            var santaPathSourceSpec = new VEShapeSourceSpecification(VEDataType.ImportXML, ‘
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/TrackingSanta.xml’, santaRouteLayer);
            santaPathSourceSpec.MaxImportedShapes = 300;
            try {
                map.ImportShapeLayerData(santaPathSourceSpec, trackSantasPathCallback, 1);
            }
            catch (e) {
                alert(e.message);
            }
        }

        function trackSantasPathCallback(layer) {
            var arrSantasPath = new Array();
            layer.Hide();
            //Loop through points to draw line
            var intPathCount = layer.GetShapeCount();

            //Put GeoRSS into an array
            //Begins at North Pole
            arrSantasPath.push(new VELatLong(84.6687, -154.1864));
            for (i = 0; i < intPathCount; i++) {
                arrSantasPath.push(layer.GetShapeByIndex(i).GetPoints()[0]);
            }
            arrSantasPath.push(new VELatLong(84.6687, -154.1864));
            //Ends at North Pole

            //Draw the trail
            var shapeSantasTrail = new VEShape(VEShapeType.Polyline, arrSantasPath);
            shapeSantasTrail.SetLineColor(new VEColor(255, 0, 0, 1));
            shapeSantasTrail.SetCustomIcon(giftImageURL);
            try {
                map.AddShape(shapeSantasTrail);
            }
            catch (e) {
                alert(e.message);
            }
            //Render the pins
            var giftPins;
            for (p = 0; p < intPathCount; p++) {
                giftPins = new VEShape(VEShapeType.Pushpin, arrSantasPath[p]);
                giftPins.SetCustomIcon(giftImageURL);
                try {
                    map.AddShape(giftPins);
                }
                catch (e) {
                    alert(e.message);
                }
            }
        }

        //Adds the North Pole Models while Santa is flying
        function AddModel(type) {
            var finalposition = new VELatLong();
            var rotat = new VEModelOrientation(60, 0, 0);

            NorthPoleLayer = new VEShapeLayer();

            finalposition.Latitude = 84.6685;
            finalposition.Longitude = -154.1865;

            // Set directory to read the models from:
            sourceDirectory = “
http://msnbcmedia.msn.com/i/MSNBC/Components/Interactives/Projects/VE/santatracker/objs/”;

            // 1st carousel
            var modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Augusto/carousel.obj”, NorthPoleLayer);
            // Load the model into the 3D map instance (model source, callback, latlong, orient, scale)
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Second House
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Stephen/H2 Second House 02/H2secondhouse02e.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // High Res House
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Stephen/HigherResHouse/HigherResHouse.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // House with windows
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Stephen/Housewithwindows02/Housewithwindows03.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Streetlamp
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Stephen/Streetlamp/Streetlamp.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Barn
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/barn.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Coral
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/coral.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Cottage
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/cottage.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Factory
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/factory.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // North Pole candy cane
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/north_pole.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Surrounding Forest
            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/tree_boards2.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }

            // Trees – position needs adjustment
            finalposition.Latitude = 84.6685 + 0.00006;
            finalposition.Longitude = -154.1865 + 0.0006;

            modelSpec = new VEModelSourceSpecification(VEModelFormat.OBJ, sourceDirectory + “Heidi/less_trees.obj”, NorthPoleLayer);
            try {
                map.Import3DModel(modelSpec, onFinalModelLoad, finalposition, rotat, null);
            }
            catch (e) {
                alert(e.message);
            }
        }

        function onSantaLoad(model, status) {
            if (status == VEModelStatusCode.Success) {
                alert(“Santa has been located!”);
                //Center camera altitude
                try {
                    map.SetAltitude(200);
                }
                catch (e) {
                    alert(e.message);
                }
            }
            if (status == VEModelStatusCode.InvalidURL) {
                alert(“The URL given for the Santa model data is invalid.”);
            }

            if (status == VEModelStatusCode.Failed) {
                alert(“There was a problem loading the Santa model.”);
            }
        }

         function onModelLoad(model, status){
            if (status == VEModelStatusCode.Success)
            {
//                alert(“Model has successfully loaded.”);
            }
            if (status == VEModelStatusCode.InvalidURL)
            {
               alert(“The URL given for the model data is invalid. Refresh the page to try again.”);
            }
            if (status == VEModelStatusCode.Failed)
            {
                alert(“There was a problem loading the 3D model. Refresh the page to try again.”);
            }
         }

         //When the final model loads, we’ll get the North Pole tile overlays.
         function onFinalModelLoad(model, status) {
             if (status == VEModelStatusCode.Success) {
                GetTiles();
             }

             if (status == VEModelStatusCode.InvalidURL) {
                 alert(“The URL given for the model data is invalid.”);
             }

             if (status == VEModelStatusCode.Failed) {
                 alert(“There was a problem loading the 3D model.”);
             }
         }

         function disposeMap() {
             map.Dispose();
         }
      </script>
    <!–Brought to you by Microsoft’s Chris Pendleton and Tom Grimes–>
    </head>
   <body style=”width:100%;height:100%;margin:0 0 0 0;” onload=”GetMap();LoadSantaGeoRSS();” onunload=”disposeMap();”>
            <div id=”myMap” style=”position:absolute; top:0px;left:0px; width:100%; height:100%;”></div>
               </body>
</html>

You know next year is going to HAVE to be better, right? Stay tuned!