Virtual Earth (VE) in NASA WorldWind (WW)
one of my goals for this year is to enter more contests. the first contest i noticed this year was the Mix06 contest. but i boycotted it based on not wanting to deal with the pain of crappy web development standards. that was a good decision because the other entries would have blown away anything i had in mind. the second contest to come up was Virtual Earth Madness, with the prize being an XBox 360! had done a bit of MapPoint development in the past, and had been meaning to kick around VirtualEarth. throw in the chance for an XBox 360 and that pushed me over the edge. just so happened i was already heading down this path. my last article /mceWorldWind, creates 3 plugins to be used in NASAs WorldWind 3D globe application. the goal for that article was to make a 4th plugin to bring VE data into WW, but i delayed that plan after finding out about the VE Madness competition.
anyway, this article will explain how to create a plugin for NASA WorldWind that will incorporate Virtual Earth's data. the benefits for WW is that VE provides a great source for maps with multiple views (road, aerial, hybrid). the benefits for VE is that WW brings the maps into a 3D world with terrain data and many other plugins for overlaying other data sources. Thanks to NASA for providing a great open source application like WW, and Thanks to Microsoft for providing terrific mapping data. i believe MS already provides TerraServer data to WW too! and just to put things in perspective ... no thanks to google who shutdown a similar plugin for bringing google maps data into WW.
VirtualEarth is the backend for local.live.com. and it provides commercial and non-commercial developer capabilities for adding maps to your web site. the best resource for developing with VE is ViaVirtualEarth.com. the supported development model is to embed a control on your web page and then script against that control. er, um ... this article doesn't use any of the supported developer hooks. this is a total HACK that goes directly against the VE tile servers. so Microsoft could change their protocol for communicating to the tile servers and break this any time they wanted. consider yourself WARNED. they just recently updated to V2 for the tile servers, so hopefully this will last for at least a little bit. also, MS has allowed the VE Mobile application (by Jason Fuller) to remain in the wild ... so that's a good sign.
the first step was to figure out how to request map tiles. one way to reverse engineer this would be to sniff the wire with some application like Fiddler when you are browsing local.live.com. instead, i just reverse engineered VE Mobile (v2) using Lutz's Reflector. to figure out which tile to get, you need to know row, column, zoom level and map type. the tiles are laid out in rows and columns and there are at least 19 zoom levels (maybe more). at zoom level 1, there are only 4 tiles (2 rows, 2 columns) to make up the entire map of the earth. if you zoom in, then each level 1 tile becomes 4 level 2 tiles. so level 2 is made up of 16 tiles (4 rows, 4 columns). if you keep zooming in, then these numbers get big real quick ...
told you ... big numbers. each tile is 256x256 pixels. the road tiles are .png files and the aerial/hybrid map types are .jpeg files. so if you want to think about even bigger numbers, then take a guess at what the average size for a 256x256 .png/.jpeg file is and then multiply that by the # of tiles above! of course they can conserve some space by just having a single file for water, etc. also, most of their data is currently for the US. if you zoom in where there is no data, then you'll get the following image
the row count goes top-down (N-S) and the column count is from left-right (W-E). and the number of rows/columns increases as the zoom level increases. it's also important to know what distance each tile represents. this can be calculated to determine meters per pixel.
and this is where i refer you to the code (linked in the 'source' section below). it's pretty easy to start with a lat/lon and then determine what row/col you should be in. with the row/col info, then you can generate the url that is needed to request the appropriate tile. map type comes into play when generating the url, it effects the url in a couple places
one other thing worth noting is the VE tiles are in a Mercator projection. this makes it kind of tricky to bring the data into WW. also, Mercator is no good around the North and South poles. the pic below was created by stitching together the 4 tiles for zoom level 1
that is all we really need to know about the tile servers.
WorldWind has a pretty powerful plugin model. i just recently used it with the /mceWorldWind article to create 3 different plugins. but i had never attempted to use the model to add an image layer into WW. luckily, the BlueMarble layer is implemented as a plugin, so i used it as a starting point. the plugin starts up and creates a Form that can be displayed to allow for user input. it also hooks the MenuBar, the ToolBar, and adds image layers to the LayerManager. excellent, the VE plugin needs all of that.
so WorldWind also has its own tiling scheme. but its rows start in the south and move up (opposite of VE), while the columns are also west to east. it also supports zoom levels so if you zoom in, then each tile is replaced by 4 tiles for higher resolution. it adds the concept of ZeroTileSizeDegrees which specifies when a certain level will be displayed and determines when zoom levels change. when you are fully zoomed out, a ZTSD of 45 or 36 degrees will work. if you don't want your layer to display at that high of altitude, then you can specify a smaller ZTSD. next, the WW globe uses EPSG:4326 (lat/lon projection) and its mesh is modeled as a perfect sphere. all of this is implemented in the QuadTile class.
er, um ... so i thought this was going to be easy. it looked like i could just reuse the existing QuadTileSet logic and then create my own ImageTileService to return the appropriate tile server url. it works by the QuadTileSet requesting a url for a tile. in this request it passes the WW row, col, and level. the ImageTileService uses that to create the appropriate url to retrieve the right texture and then returns that url. then the QuadTileSet downloads that image and uses it as a texture for the mesh.
my first attempt was to create my own ImageTileService. it would get the WW row/col/level and then convert that to the corresponding VE row/col/level. then that could be used to create the VE url for the tile and magic would happen. well ... it worked ... in theory. but i could never get things to line up just right. actually, the tiles did line up at the equator, but as you moved north or south (away from the equator), then the tiles were just not lining up correctly. the first problem is that a row of tiles would be occasionally skipped. the image below is wrong (i have dyslexic tendencies), but look at 've rowXcol' (which should really read colXrow). if you look at the top tile and then look at the one below it, you'll see that the rows jump from 20 to 22, then it correctly goes to 23. so if that were displaying actual VE tiles, then the map would just be wrong. an entire row of images would be missing.
changed things up a bit, and made it so that it would not skip rows like above, but then the problem was that the map looked right, but nothing was in the right lat/lon coordinates. e.g. if i went to my own lat/lon, then the map was showing canada or illinois (forget if it was above or below?). but it definitely wasn't milwaukee. at this point i was pretty convinced that the problem was caused by VE being projected in Mercator. the thought was that WW was creating the meshes for a different projection, and i wasn't going to be able to request the VE tiles that correctly mapped to those meshes.
NOTE i gave up pretty quickly on this technique because i was time boxed by the competition deadline. need to revisit this to fully convince myself. it's quite possible that i was just making a silly mistake and couldn't see it at the time
the next step was to take more control. i started out by understanding Mashi's existing plugin : Map projection Plug-in. the plugin takes a single image map of the world in sinusoid projection (upside down), and it uses Proj.4 to reproject into lat/lon coordinates and render in WW. Proj.4 is a library provided by the USGS that converts cartesian coordinates to/from lat/lon using different projection techniques. this is almost exactly what i needed. the VE tiles are represented as cartesian coordinates (in meters) and i need to reproject into lat/lon for WW. and Proj.4 also supports the Mercator projection and spherical ellipses. perfect! so i started with the core of that codebase and extended it.
the first step was to get a single tile to display. created my own custom layer by inheriting from RenderableObject. for the Update method it determines the current lat, lon, and zoom level. with that info it can determine which tile to request from the VE tile server. it can also determine the extents of that tile and use Proj.4 to create the appropriate mesh. then the Render method just has to set the texture and draw the mesh ... success! the pic below shows how closely the tile lines up
in real life, it wasn't that simple. the first attempt was real close, but it was about half a tile too high. that error was removed by changing the Proj.4 ellipse parameter from 'WGS84' to 'sphere', because WW is modeled as a perfect sphere (even though the earth is a bit squashed). now it was time to add features ...
tiling. didn't think i had time to figure out how to insert this new logic into the QuadTileSet class, so i decided to come up with my own tiling scheme for V1. it works by knowing what tile you are currently focused on and what the zoom level is. from the current tile, it creates neighbor tiles outwards in square patterns. it requests tiles up to 3 rows/cols out from your current position. if you pan at the same level, then it just has to request the new tiles and dispose of the tiles no longer in view. if you change zoom level, then it requests a whole new set of tiles. the pic below shows a set of VE tiles being rendered.
downloading. it uses the WW WebDownload class to download the tiles from the VE tile servers. this should support any proxies you have setup (wasn't able to test this). also, it lets you see what is happening in the DownloadManager (Ctrl-h). it does not render any progress bar in the lower-right corner, but it does outline currently downloading tiles with a red square. the pic below shows the download manager and the red rectangles where tiles were being downloaded
caching. it saves the downloaded tiles to disk as .png or .jpeg files accordingly. if the file has already been cached, then it won't be downloaded again. this improves performance and takes some load off of the VE tile servers. it purposefully does not save to .dds because it is such a large file size and eats up disk space too fast
terrain. had to extend the code a little for creating meshes, but you can adjust the VerticalExaggeration and see the terrain. i consider this one of the primary reasons for adding VE data into WW. 3D is a goodness. the pic below shows terrain. it's cool to follow the roads through the mountains.
map types. it can display the road, aerial, and hybrid maps. hybrid is definitely my favorite. the pic below shows a top down view of hybrid tiles.
starting zoom level. this is similar to WW ZeroLevelTileSize. you can use this to change when WW tiles start to appear. if you set it to 3, then VE tiles will always display, even if you zoom all the way out. if you set it to a higher zoom level, then VE tiles won't display until you zoom in really close. the pic below shows VE tiles being displayed at a low starting zoom level.
locate me. you can click a button to have it zoom in on your location in the world based on ip address. the pic below shows the VE Form for interacting with the VE layer. you can open the form by using the VirtualEarth icon on the toolbar.
link to local.live.com. if you click this button then it will open a browser showing the same view that you have open in WW. you might do this to see Bird's Eye imagery. added this in as an afterthought, but it's proven very useful. the pic below shows a side by side view in WW and local.live.com
find address and find business. this code was taken directly from VE Mobile. it will add a PushPin for each business found. you can search for a specific location and it will zoom to that location, or it will just search the current visible viewport. the pic below shows a search for Hooters in Wisconsin.
and you can use the VE map data with many of the other WW plugins that have already been created! i also consider this a primary motivator for getting VE data into WW. the pic below shows the VE layer in the Layer Manager and the toolbar icon for opening the VE Form.
not all the features i wanted to get in, but not a bad start for V1.
the following video shows the Virtual Earth maps in WorldWind
1) NASA's WorldWind needs to be installed. NOTE WorldWind requires the .NET and DirectX runtimes to be installed too. the source code is available.
2) Copy 'proj.dll' to where WorldWind.exe installed C:\Program Files\NASA\World Wind 1.3\proj.dll. 'proj.dll' is a native DLL used to reproject VirtualEarth's Mercator map tiles into WorldWind's spherical projection. the original source can be found at http://proj.maptools.org/
3) create 'VirtualEarth' plugins folder C:\Program Files\NASA\World Wind 1.3\Plugins\VirtualEarth\
4) copy 4 files to 'VirtualEarth' plugin folder : VirtualEarthPlugin.png, VirtualEarthPushPin.png, VirtualEarthPlugin.cs, VirtualEarthPlugin.resx. this is my code. it will automatically be compiled when WorldWind starts up
5) configure WorldWind to run the plugin -start 'World Wind 1.3' (Earth) -from the menu bar, select 'Plugins' - 'Load/Unload...' -click on 'Virtual Earth Plugin' text -click 'Load' -select 'Startup' checkbox. the pic below shows the dialog for loading the VE plugin.
you can now zoom in on the Earth to see the VE tiles. the default setting is to have the tiles start appearing below an altitude of 300km. also, the best VE data is currently available for the US.
if you want to step through the code in Visual Studio, you'll need to grab the NASA WorldWind source code and open its solution. then add the code to the \WorldWind project and include the source files in the project. then you should be able to start debugging.
so i've already written this twice, i'll need some time away before i'm ready to tackle V2. if the WW community wants to take over V2, then that is fine too. but i think we should wait a bit before starting ... to see what Microsoft's reaction is, because none of this uses supported VE APIs. i'd really like some sort of good faith nod from MS before starting again. at least that they won't break it that badly :)
anyway, i cranked this out in a rush, so it definitely needs a rewrite. and we can always add features. the following is a list of possible tasks for V2
probably a DirectShow article for MCE. later