Mapping Mashups with the JXMapViewer Blog

Version 2


    In the last few years, mapping technology has advanced to the point where one can combine worldwide street maps with photos, videos, satellite images, and even bathroom locations. Thanks to the JXMapViewer, you can bring mapping technology into your own desktop Java applications.

    In the article " Building Maps into Your Swing Application with the JXMapViewer," I showed you how to build a simple mapping application using theJXMapKit, a prefab version of theJXMapViewer. In this article we will customize theJXMapViewer with graphic overlays, polygons, rollovers, and a custom map server, and then build a mashup with an external web service to search Wikipedia.

    This article will not cover how to download theJXMapViewer .jars and build a basic mapping application. You should first read the previous article, and then create a new Java desktop application and add the JXMapKit component to your main panel. Once you are done, your application should look like Figure 1.

    Figure 1. Basic desktop Java application with JXMapKit in the NetBeans form editor
    Figure 1. Basic desktop Java application withJXMapKit in the NetBeans form editor

    Custom Overlays

    In the previous article, I showed you how to use the standardWaypointPainter to draw waypoints on the map. This is a great way to draw waypoints on top of the map with a custom look, but there is much more you can do. You might want to draw static text on the map (static meaning it doesn't move when the user pans the map) or maybe some lines and polygons between your waypoints. Since overlays are Painters, they are ultimately just Java2D code underneath, letting you draw pretty much anything you want.

    Let's start by adding some static text to thank the Blue Marble map provider, NASA:

    Painter<JXMapViewer> textOverlay = new Painter<JXMapViewer>() { public void paint(Graphics2D g, JXMapViewer map, int w, int h) { g.setPaint(new Color(0,0,0,150)); g.fillRoundRect(50, 10, 182 , 30, 10, 10); g.setPaint(Color.WHITE); g.drawString("Images provided by NASA", 50+10, 10+20); } }; jXMapKit1.getMainMap().setOverlayPainter(textOverlay); 

    The Painter above will draw the text "Images provided by NASA" near the top edge of the map. The semi-transparent black rounded rectangle is crated by adding an extra opacity (alpha) argument to the Colorconstructor. To add this painter to your map you should put it in the constructor of your View, just after the call toinitComponents(). The View is the autogenerated main of your application (in my case,, created when you build a new desktop Java application. The setOverlayPainter()method adds the overlay to the map. When you run the application, it will look like Figure 2.

    Figure 2. Static text overlay
    Figure 2. Static text overlay

    Custom Polygon Overlay

    Drawing text and waypoints is nice, but to be really useful you want to draw things based on actual geographical data. Let's say you have four coordinates that define the legal boundaries of the island of Sicily. You could draw a polygon over this region to indicate where where the actual boundaries are. This is a little tricky, though. In order to draw a geographical coordinate, you must first convert it into a screen coordinate. This requires geographical transformations, which are complicated and vary by map projection type. You must also account for the user's current zoom level and panning location within the map. Fortunately, theJXMapViewer contains methods to do this conversion for you. The map.getTileFactory().geoToPixel() method converts a GeoPosition to a pixel coordinate in theworld bitmap.

    There are three important coordinate systems to understand when using the JXMapViewer. First, latitude and longitude coordinates on the actual Earth are expressed in degrees. They are represented with the GeoPosition class. This is themap coordinate system. TheTileFactory.geoToPixel() method convertsGeoPositions into pixels in the world bitmap, which is the bitmap that would be created if you drew every tile at a given zoom level on the screen at once. This is the world bitmap coordinate system. The 0,0 pixel would be in the upper left hand corner near Alaska and the maximum x/y pixel would be in the lower right-hand corner near Antarctica. Of course, this isn't what you see onscreen. Onscreen, you see just portion of the world bitmap drawn into the JXMapViewer's viewport. To calculate the offset, you can call your JXMapViewer'sgetViewportBounds() method to get the current size and position of the viewport. This will let you convert points into the third coordinate system: screen coordinates. Screen coordinates are what you use for drawing and accepting mouse input. If you understand how to convert among these three coordinate systems, you can draw anything you want on the map.

    The code below draws a polygon using four coordinates representing the bounds of Sicily.

    final List<GeoPosition> region = new ArrayList<GeoPosition>(); region.add(new GeoPosition(38.266,12.4)); region.add(new GeoPosition(38.283,15.65)); region.add(new GeoPosition(36.583,15.166)); region.add(new GeoPosition(37.616,12.25)); Painter<JXMapViewer> polygonOverlay = new Painter<JXMapViewer>() { public void paint(Graphics2D g, JXMapViewer map, int w, int h) { g = (Graphics2D) g.create(); //convert from viewport to world bitmap Rectangle rect = map.getViewportBounds(); g.translate(-rect.x, -rect.y); //create a polygon Polygon poly = new Polygon(); for(GeoPosition gp : region) { //convert geo to world bitmap pixel Point2D pt = map.getTileFactory().geoToPixel(gp, map.getZoom()); poly.addPoint((int)pt.getX(),(int)pt.getY()); } //do the drawing g.setColor(new Color(255,0,0,100)); g.fill(poly); g.setColor(Color.RED); g.draw(poly); g.dispose(); } }; 

    The code above defines the four coordinates and then draws a polygon based on those coordinates. The important code is the lineg.translate(-rect.x, -rect.y), which converts the graphics context into the world bitmap coordinates. Then, inside of the region loop, the code callsmap.getTileFactory().geoToPixel() to convert theGeoPositions into the world bitmap space as well. Finally, it draws the polygon onscreen using a translucent red so the map will show through. The finished product looks like Figure 3.

    Figure 3. Sicily polygon overlay
    Figure 3. Sicily polygon overlay

    You can combine the text overlay with the polygon overlay using a CompoundPainter. A CompoundPainter is a special Painter that aggregates any number of otherPainters into a single stack drawn in order. Be sure to set the cacheable property to false or else the polygon Painter won't update when the user drags the map.

    CompoundPainter cp = new CompoundPainter(); cp.setPainters(textOverlay,polygonOverlay); cp.setCacheable(false); jXMapKit1.getMainMap().setOverlayPainter(cp); 

    Waypoint Rollovers

    The JXMapViewer was originally created for a JavaOne demo called Aerith. One of the cool features in Aerith was a nice rollover effect when you moved the mouse over a waypoint. A small window would pop up showing a thumbnail of a photo. The APIs for the JXMapViewerhave changed significantly since Aerith was released, so the old rollover method won't work anymore (not that you'd want to copy Aerith anyway, since it was written on such a short deadline and much of the code is a bit dicey). However, you can still create rollovers with a different method.

    A rollover can be implemented in one of two ways. First, you can install another overlay painter that draws the overlay. But this won't work if you want the rollover to be interactive in any way, such as a button or text field. The second way is to use a real Swing component. The JXMapViewer is aJPanel subclass, so you can implement rollovers by adding your components as children of the JXMapViewerand conditionally showing them when the mouse is near a waypoint.

    Below is a small example of a rollover. It is aJLabel that will appear when the user moves the mouse near the island of Java.

    final JLabel hoverLabel = new JLabel("Java"); hoverLabel.setVisible(false); jXMapKit1.getMainMap().add(hoverLabel); jXMapKit1.getMainMap().addMouseMotionListener(new MouseMotionListener() { public void mouseDragged(MouseEvent e) { } public void mouseMoved(MouseEvent e) { JXMapViewer map = jXMapKit1.getMainMap(); //location of Java GeoPosition gp = new GeoPosition(-7.502778, 111.263056); //convert to world bitmap Point2D gp_pt = map.getTileFactory().geoToPixel(gp, map.getZoom()); //convert to screen Rectangle rect = map.getViewportBounds(); Point converted_gp_pt = new Point((int)gp_pt.getX()-rect.x, (int)gp_pt.getY()-rect.y); //check if near the mouse if(converted_gp_pt.distance(e.getPoint()) < 10) { hoverLabel.setLocation(converted_gp_pt); hoverLabel.setVisible(true); } else { hoverLabel.setVisible(false); } } }); 

    The core of the code above is a MouseMotionListeneron the mainMap, which converts aGeoPosition representing the island of Java into screen coordinates. Then it tests if the mouse is near the converted point and conditionally shows moves and shows the component. This example just uses a JLabel, but you could easily use any other component, or an entire panel. When you run the application it will look like Figure 4.

    Figure 4. Rollover near the island of Java
    Figure 4. Rollover near the island of Java

    Though the JXMapViewer (and JXMapKit) look pretty plain by default, with painters or custom drawing code you can completely customize them. In Figure 5 is an applet I wrote using the JXMapKit but tricked out with customPainters. You can't see it in this screenshot, but there are rollover glow effects for the components and animation between each waypoint.

    Figure 5. A fully tricked out JXMapViewer
    Figure 5. A fully tricked-out JXMapViewer

    Using a Custom Map Server

    The JXMapViewer comes preconfigured with connections to Open Street Map and the Blue Marble, but you may want to connect to a different map server. By selectingCustom for the JXMapKit'sdefaultProvider property, you can use a custom tile provider to connect to an alternate map server.

    The JXMapViewer (and JXMapKit) have atileFactory property, which accepts instances of theTileFactory abstract class. This is the class that actually loads the image tiles, caches them, and manages the thread pooling. You can create your own implementation of theTileFactory class to completely change how theJXMapViewer loads images, but this is a lot of work and not necessary most of the time. Instead, you can configure theDefaultTileFactory with theTileFactoryInfo class. TileFactoryInfocontains a bunch of information about the map (such as the size of the tiles and how many there are) and a single method,getTileURL(). By creating an anonymous subclass ofTileFactoryInfo, you can make theJXMapViewer connect to pretty much any map server you want.

    Suppose you would like to load tiles from a directory on disk instead of a web server. These files are in the directory/MyHarddrive/test/maptiles/ and are named with the schemex_y_z.jpg. You can easily load these images by overriding the getTileURL method to return the appropriatefile: URLs.

    TileFactoryInfo info = new TileFactoryInfo( 0, //min level 8, //max allowed level 10, // max level 256, //tile size true, true, // x/y orientation is normal "file:/MyHarddrive/test/maptiles", // base url "x","y","z" // url args for x, y & z ) { public String getTileUrl(int x, int y, int zoom) { return this.baseUrl + x+"_"+y+"_"+"zoom"+".jpg"; } }; jXMapKit1.setTileFactory(new DefaultTileFactory(info)); 

    Notice the arguments to the DefaultTileProviderconstructor. These arguments detail the map metrics. It's very important to understand these arguments, so I will go through them carefully. Every zoomable map is essentially a pyramid of stacked images, where each level is a zoomed-in version of the same data as the previous level. This also means that each level has four times the number of images as the level before. The first four numbers in the constructor above describe the image pyramid. The first number is the lowest zoom level of the pyramid (usually 0). The second number is the highest zoom level the user is allowed to navigate to. The third number is the top level of the pyramid (the second and third numbers will be the same if you allow users to navigate through the entire set of zoom levels). The last of the number arguments is the size of each tile in pixels (they must be square).

    After the number arguments are two Booleans and fourStrings. The Booleans set whether the xand y axes are normal or flipped (meaning they go from 0 to N or from -N/2 to 0 to N/2). The four strings are the base URLs of images and HTTP parameters used for the x, y, and z values. The default implementation of getTileUrl() will use these four strings to generate image URLs. If those values aren't enough to specify your image URLs, then you can override thegetTileUrl() method to generate URL strings directly. In the example above, the code generates file: URLs using the baseURL variable (a protected field in theDefaultTileProvider that contains the base URL passed into the constructor) and the x, y, andzoom parameters passed into thegetTileUrl() method.

    Some maps are more complicated than the example above. The World of Warcraft map server at, for example, is similar to the standard map layout, but its tiles are split over two servers and the zoom levels are the reverse of normal. Below is the a tile configuration that can connect to the Mapwow tile server.

    TileFactoryInfo info = new TileFactoryInfo( 0, //min level 8, //max allowed level 9, // max level 256, //tile size true, true, // x/y orientation is normal "", // base url "x","y","z" // url args for x, y and z ) { public String getTileUrl(int x, int y, int zoom) { int wow_zoom = 9-zoom; String url = this.baseURL; if(y >= Math.pow(2,wow_zoom-1)) { url = ""; } return url + "zoom"+wow_zoom+"maps/" +x + "_" + y + "_" + wow_zoom +".jpg"; } }; jXMapKit1.setTileFactory(new DefaultTileFactory(info)); 

    The resulting map looks like Figure 6.

    Figure 6. World of Warcraft map server
    Figure 6. World of Warcraft map server

    Note that since we haven't taken out the painters from the earlier example, we're giving NASA credit for satellite photos of WoW's fictional world of Azeroth. Cleaning that up is left as an exercise.

    Creating a Mashup

    What really put geo applications on the map (so to speak) aremashups. In 2005, shortly after the release of Google Maps, an enterprising developer created a web application that combined Google Maps with Craigslist to show homes for sale by location. This style of data source mixing became know as a "mashup," and the world of web services was never the same again.

    In our final example we will hook the JXMapViewerup to a web service at, which will returnWikipedia articles with locations related to a search query and then plot these articles on the map. To do this we will use new functionality in NetBeans 6 to make the work a lot easier.

    Create a search field, label, and search button on the canvas (Figure 7). Create an action for the search button by right clicking on it and selecting "Set Action...". UsesearchWikipedia for the action method and turn on the "Background Task:" checkbox to make the action use a background thread. NetBeans will generate a searchWikipediamethod with a SearchWikipediaTask object to handle the threading. We will put the actual searching and waypoint code into the doInBackground() and succeeded()methods of this SearchWikipediaTask class.

    Figure 7. Search form with map
    Figure 7. Search form with map

    With the empty methods created we can now write code to connect to the GeoNames web server. The GeoNames search web service takes a query string to search Wikipedia (q) and a maximum number of rows to return (maxRows). The web service returns an XML file containing entry elements. The code to parse it looks like this:

    @Override protected Set<WikiWaypoint> doInBackground() { try { // example: URL url = new URL(""+ jTextField1.getText()+"&maxRows=10"); XPath xpath = XPathFactory.newInstance().newXPath(); NodeList list = (NodeList) xpath.evaluate("//entry", new InputSource(url.openStream()), XPathConstants.NODESET); Set<WikiWaypoint> waypoints = new HashSet<WikiMashupView.WikiWaypoint>(); for(int i = 0; i < list.getLength(); i++) { Node node = list.item(i); String title = (String) xpath.evaluate("title/text()", node, XPathConstants.STRING); Double lat = (Double) xpath.evaluate("lat/text()", node, XPathConstants.NUMBER); Double lon = (Double) xpath.evaluate("lng/text()", node, XPathConstants.NUMBER); waypoints.add(new WikiWaypoint(lat, lon, title)); } return waypoints; // return your result } catch (Exception ex) { ex.printStackTrace(); return null; } } 

    The code above uses XPath queries to grab all entryelements and then extract the title, lat, and lng elements. For each entry the code creates aWikiWaypoint, which is just a subclass ofWaypoint with an extra field to store the title of the entry. All of this code is inside the doInBackgroundmethod of the SearchWikipediaTask class. As the name suggests, this method will be run on a background thread automatically. Since you shouldn't modify Swing components from background threads, this method will return the Set ofWikiWaypoints. This Set is then passed to the succeeded method of the Task, which will automatically be called on the proper Swing thread. Below is the implementation of the succeeded method, which draws all of the waypoints on the map using a customWaypointRenderer.

    @Override protected void succeeded(Set<WikiWaypoint> waypoints) { // move to the center jXMapKit1.setAddressLocation(waypoints.iterator().next().getPosition()); WaypointPainter painter = new WaypointPainter(); //set the waypoints painter.setWaypoints(waypoints); //create a renderer painter.setRenderer(new WaypointRenderer() { public boolean paintWaypoint(Graphics2D g, JXMapViewer map, Waypoint wp) { WikiWaypoint wwp = (WikiMashupView.WikiWaypoint) wp; //draw tab g.setPaint(new Color(0,0,255,200)); Polygon triangle = new Polygon(); triangle.addPoint(0,0); triangle.addPoint(11,11); triangle.addPoint(-11,11); g.fill(triangle); int width = (int) g.getFontMetrics().getStringBounds(wwp.getTitle(), g).getWidth(); g.fillRoundRect(-width/2 -5, 10, width+10, 20, 10, 10); //draw text w/ shadow g.setPaint(Color.BLACK); g.drawString(wwp.getTitle(), -width/2-1, 26-1); //shadow g.drawString(wwp.getTitle(), -width/2-1, 26-1); //shadow g.setPaint(Color.WHITE); g.drawString(wwp.getTitle(), -width/2, 26); //text return false; } }); jXMapKit1.getMainMap().setOverlayPainter(painter); jXMapKit1.getMainMap().repaint(); } 

    Most of the method above is taken up by the Java2D code that draws a translucent round rectangle tab with the title of the article within it. Be sure to notice the first line, which recenters the map on the first waypoint. When you run the program it will look like Figure 8.

    Figure 8. Searching for Java on Wikipedia
    Figure 8. Searching for Java on Wikipedia


    The mashup example in this article is pretty simple, but in an advanced version you could easily add thumbnails, summary text, and links to the real Wikipedia articles. This mashup merely touches on the possibilities unleashed when you combine mapping technology with other web services. Just take a look at the developer portals of Google and Yahoo to see what other people are doing with mashups.

    Now that you know how to build mashups in Swing using theJXMapViewer, what cool applications can you think of? If you build something interesting, please post a comment and link below so we can put it on Mapping is a big part of the future, and desktop Java is right there with it.