Getting started with the Aerith Mapping Component

A few days ago we released the code to Aerith, our JavaOne demo that combines photos, mapdata, and 3d effects. We worked very hard to get the code out to you and let you see how everything works. However, if you've downloaded the code you may have noticed that the code for the map parts is missing. Only the binaries are provided in the JXMapViewer.jar file. That's because the map component has a brighter future than just a JavaOne demo. It is now the first component in our new SwingLabs project: The Swing Web Services components, or SwingX-WS.

I'm really excited about this. Doing interesting things with web services has always been our dream for SwingLabs, and Desktop Java in general. Now we have a place to put it all. And the JXMapViewer is only the beginning. We want to have all sorts of components in there for things like Flickr, Del.icio.us, and RSS/Atom feeds. It's a good time to be a client developer!

Okay, so on to more immediate things. How do you use the map viewer? Just add it to your panel and you are ready to go. If you add it to your NetBeans palette you can just drag it into your form, hit F6, and see it come up. It'll look something like this:

emptytileprovider.jpg

What? Where's the map?!

Okay, I lied. So there is a bit more you have to do. The JXMapViewer uses a TileFactory implementation to retrieve the actual tiles. By default it uses an EmptyTileFactory which gives you the result you see here, which isn't very useful but can be helpful when testing code. For something more interesting you can provide your own TileFactory implementation, or use the DefaultTileFactory.

The DefaultTileFactory implements the basic tile loading and geometric transformation behavior required for your typical tile based map server like Google Maps and Yahoo Maps (and presumably others, though I haven't tested it). You just need to provide information about the server to the DefaultTileFactory and it will take care of the rest. You do this using an instance of TileProviderInfo. For example, you could connect to an internal tile map server using something like this:

TileProviderInfo info = new TileProviderInfo(
   0,    // minimum zoom
   15,   // maximum zoom
   17,   // total zoom levels (only 0 - 15 can be viewed)
   256,  // square tile size - 256x256
   true, // x increase to the right
   true, // y origin is at north pole (not the equator)
   "http://myserver.com/getmap.jsp?" // tile server base URL
   "x","y","z" // x,y and zoom URL parameter names
);
TileFactory fact = new DefaultTileFactory(info);
JXMapViewer map = new JXMapViewer();
map.setFactory(fact);
map.setZoom(7);
map.setAddressLocation(new GeoPosition(37.392137,-121.950431));

The first four numbers to the TileProviderInfo constructor represent the minimum zoom, the maximum zoom, the total zoom levels, and the tile size. The two booleans are used to indicate if the x coordinates go left to right or right to left and if the y coordinate goes from top to bottom or out from the equator. The rest of the parameters are for the base url and the name of the http parameters in the get request to fetch tiles. Once the TileProviderInfo is create you can build a DefaultTileFactory, install it on a new JXMapViewer, then set the starting zoom level and location. The GeoPosition above is the latitude and longitude coordinates of my office (give or take a few hundred feet. :).

That's pretty much all you need, but the zoom levels may take some explaining. A tile based map is essentially a pyramid of square bitmaps. At the top level you have a single bitmap with a certain size, in this case 256 x 256 pixels. In the level below that there are four tiles, also 256 x 256 each. On the third level there are four tiles for each tile in the second level, for a total of sixteen. When you continue on down the number of tiles grows by a factor of four. As you can imagine this creates a lot of tiles! The total zoom levels is the number of levels in your tile pyramid. The minimum and maximum zoom levels are the limits placed on the user navigating within the levels, since you might not want the user to go all the way to the top or bottom (maybe because you don't have real tiles there). In any case, you can customize these to match your own tile server and then let the DefaultTileFactory handle the details.

At JavaOne we demonstrated the JXMapViewer, as part of the Aerith demo, with a TileProviderInfo class that pointed at Google's tile server. This was relatively easy to do, since the REST web service they provide is straightforward. We have not published the settings for using that server because we have not yet received formal approval for doing so from Google. JXMapViewer is a generally useful component for displaying large tiled images from any source. We didn't want to suspend its release until we'd gained approval for demonstrating it with Google's tile server, however we hope to do so in the future.

The World of Warcraft maps

Here's a working example. The guys over at MapWow.com have created a map of the World of Warcraft using data from actual players. It uses an api similar to Google Maps so it's very easy to plug into:

//wowmap version
TileProviderInfo info = new TileProviderInfo(0,8,9, // 9 levels, navigate from 0 to 8
    256, true, true, // tile size is 256 and x/y orientation is normal
    "http://mapwow.com/gmap/zoom",
    "x","y","z") {
        public String getTileUrl(int zoom, TilePoint tilePoint) {
            return this.baseURL + zoom + "maps/"+
                   tilePoint.getX()+"_"+tilePoint.getY()+"_"+zoom+".jpg";
        }
    };
map.setZoom(6);
map.setAddressLocation(new GeoPosition(50,-80));

The code above looks pretty similar to the previous example except that I have to override getTileUrl with a custom version. That's because the MapWow urls don't use a format compatible with default version. Their urls use the zoom variable in two places so I create a two line method to generate the proper url. When you run the map it will look like this:

mapwow.jpg

WoW Map

Or you can run it right now if you have Java 5 or higher installed.

Using a single tile

Now lets supposed instead of a big remote map composed of a bunch of tiles you'd like to just use a single image stored locally, say a satellite photo from Nasa. You can do that too using a different TileProviderInfo. You just need to override getTileUrl() to return the base url without any parameters. Then set all of the zoom parameters to 1 and the tile size to the size of your image.

//single square file version
TileProviderInfo info = new TileProviderInfo(1, 1, 1, 640, true, true,
	"file:/Users/josh/Desktop/world.small.square.jpg",
   "x","y","z") {
		 public String getTileUrl(int zoom, TilePoint tilePoint) {
        	return this.baseURL;
        }
    };

singletile.jpgSingle 640px NASA image of earth

Making your own tile images from NASA photos

Lets say you have a gigantic NASA image that you'd like to navigate through. (There are tons of great images here.) First you'll need to convert the image into a pyramid of tiles and then construct a TileProviderInfo to load the images from disk. To convert the images you need to chop up the main image into 256x256 tiles, save them to a directory according to a particular naming scheme, scale the image to half the size, split *that* image into tiles, and repeat until you have only a single tile left. It'd sure be nice to have a program that does this for you. As it happens I created just such a program.

tilecutter.jpg The ImageTileCutter

The tile generator will take any image readable by ImageIO, scale and pad it to become a square, and then start generating tiles. Most of the parameters are hard coded right now (tile size, directory names, etc) but that could be changed in the future. Using this program I was able to create a tile set that looks like this:

outputimage.5-0-0.jpg the top most image of the cloudless earth image stack

I should mention that this program is horribly naive and not efficient at all. If you deal with large images you will need to increase the memory you give it on the command line with something like: -Xms600m -Xmx600m. I was able to chop down a NASA photo at 5400x2700 pixels in about 30 seconds on my dual-core 2ghz MacBook. I haven't tried anything larger yet but I'm sure the task will scale exponentially. In the future I'd like to see a toolset that uses ImageMagick or Java Advanced Imaging for fast non-interactive scaling. (perhaps there's a java.net project idea in there?!)

You can get the source to the tile generator as a netbeans project here

Once you have a directory structure full of tiles you need to load them into your map. My set uses 5 levels of tiles stored in the format: basedir/tiles/zoomlevel/outputimage.zoom-x-y.jpg. The TileProviderInfo to load them looks like:

//local 5 level tile set of the globe produced by ImageTileCutter
TileProviderInfo info = new TileProviderInfo(
        0, 5, 5, 256, true, true,
        "file:/Users/joshy/projects/java.net/ImageTileCutter/tiles", "x","y","z") {
            public String getTileUrl(int zoom, TilePoint tilePoint) {
                return this.baseURL + "/"+zoom+"/"+"outputimage."+zoom+"-" + tilePoint.getX() + "-"+tilePoint.getY()+".jpg";
            }
        };

and the finished map looks like this:

If you'd like to try it online you can connect to some tiles that Hans Muller created from a Hubble image at http://download.java.net/javadesktop/hubble/tile_row_column.jpg:

        // hans tiles
        TileProviderInfo info = new TileProviderInfo(1, 1, 7, 200, true, true,
            "http://download.java.net/javadesktop/hubble/tile_",
            "x","y","z") {
                public String getTileUrl(int zoom, TilePoint tilePoint) {
                    return this.baseURL + tilePoint.getY() + "_" +
                            tilePoint.getX()+".jpg";
                }
            };
        map.setZoom(1);

With tiles that look like this:

Conclusion

The JXMapViewer is a powerful component that can be modified to talk to any map server. In this blog I have only scratched the surface of what this component can do. In a future blog I will dig into the Overlay API that lets you add waypoints, paths, and draw on top of the map, and the Yahoo Geocoder that converts street addresses into lat/long coordinates.

the JXMapViewer is functional but it has a lot of bugs and limitations. In particular the viewer assumes tiles are square, there are repaint glitches, the threading code is overly complicated, and the lat/lon conversion only works with certain kinds of mercator projections. Please join the new SwingX-WS project to play with the components, help fix bugs, add features, and contribute new components.

Links

Talk to me about it on Twitter

Posted July 11th, 2006

Tagged: java.net