Skip to main content

Mapping Mashups with the JXMapViewer

November 13, 2007

{cs.r.title}







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 "http://www.safe2pee.org/beta/">bathroom locations. Thanks to
the JXMapViewer, you can bring mapping technology into
your own desktop Java applications.

In the article " "http://today.java.net/pub/a/today/2007/10/30/building-maps-into-swing-app-with-jxmapviewer.html">
Building Maps into Your Swing Application with the JXMapViewer
,"
I showed you how to build a simple mapping application using the
JXMapKit, a prefab version of the
JXMapViewer. In this article we will customize the
JXMapViewer 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 the
JXMapViewer .jars and build a basic mapping
application. You should first read "http://today.java.net/pub/a/today/2007/10/30/building-maps-into-swing-app-with-jxmapviewer.html">
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.

<br "Figure 1. Basic desktop Java application with JXMapKit in the NetBeans form editor" />

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

Custom Overlays

In the previous article, I showed you how to use the standard
WaypointPainter 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&lt;JXMapViewer&gt; textOverlay = new Painter&lt;JXMapViewer&gt;() {
    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 Color
constructor. To add this painter to your map you should put it in
the constructor of your View, just after the call to
initComponents(). The View is the
autogenerated main of your application (in my case,
WikiMashupView.java), 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.

<br "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, the
JXMapViewer contains methods to do this conversion for
you. The map.getTileFactory().geoToPixel() method
converts a GeoPosition to a pixel coordinate in the
world bitmap.

ca -->

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 the
map coordinate system. The
TileFactory.geoToPixel() method converts
GeoPositions 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's
getViewportBounds() 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&lt;GeoPosition&gt; region = new ArrayList&lt;GeoPosition&gt;();
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&lt;JXMapViewer&gt; polygonOverlay = new Painter&lt;JXMapViewer&gt;() {

    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 line
g.translate(-rect.x, -rect.y), which converts the
graphics context into the world bitmap coordinates. Then, inside of
the region loop, the code calls
map.getTileFactory().geoToPixel() to convert the
GeoPositions 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.

<br "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 other
Painters 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 "https://aerith.dev.java.net/">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 JXMapViewer have 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 a
JPanel subclass, so you can implement rollovers by
adding your components as children of the JXMapViewer
and conditionally showing them when the mouse is near a
waypoint.

Below is a small example of a rollover. It is a
JLabel 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()) &lt; 10) {
            hoverLabel.setLocation(converted_gp_pt);
            hoverLabel.setVisible(true);
        } else {
            hoverLabel.setVisible(false);
        }
    }
});

The core of the code above is a MouseMotionListener
on the mainMap, which converts a
GeoPosition 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.

<br "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 "http://maps.joshy.net/applet/fancyapplet.html">applet I wrote
using the JXMapKit but tricked out with custom
Painters. You can't see it in this screenshot, but
there are rollover glow effects for the components and animation
between each waypoint.

<br "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 selecting
Custom for the JXMapKit's
defaultProvider property, you can use a custom tile
provider to connect to an alternate map server.

The JXMapViewer (and JXMapKit) have a
tileFactory property, which accepts instances of the
TileFactory 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 the
TileFactory class to completely change how the
JXMapViewer loads images, but this is a lot of work
and not necessary most of the time. Instead, you can configure the
DefaultTileFactory with the
TileFactoryInfo class. TileFactoryInfo
contains 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 of
TileFactoryInfo, you can make the
JXMapViewer 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 scheme
x_y_z.jpg. You can easily load these images by overriding
the getTileURL method to return the appropriate
file: 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 &amp; 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 DefaultTileProvider
constructor. 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 four
Strings. The Booleans set whether the x and 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 the
getTileUrl() method to generate URL strings directly.
In the example above, the code generates file: URLs
using the baseURL variable (a protected field in the
DefaultTileProvider that contains the base URL passed
into the constructor) and the x, y, and
zoom parameters passed into the
getTileUrl() method.

Some maps are more complicated than the example above. The World
of Warcraft map server at "http://mapwow.com/">mapwow.com, 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
        "http://wesmilepretty.com/gmap2/", // 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 &gt;=  Math.pow(2,wow_zoom-1)) {
            url = "http://int2e.com/gmapoutland2/";
        }
        return url + "zoom"+wow_zoom+"maps/" +x + "_" + y + "_" + wow_zoom +".jpg";
    }
};

jXMapKit1.setTileFactory(new DefaultTileFactory(info));

The resulting map looks like Figure 6.

<br "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) are
mashups. In 2005, shortly after the release of Google Maps,
an enterprising developer created a web application that combined
Google Maps with "http://craigslist.com/">Craigslist to show "http://www.housingmaps.com/">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 JXMapViewer
up to a web service at "http://www.geonames.org/">GeoNames.org, which will return
Wikipedia 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...". Use
searchWikipedia for the action method and turn on the
"Background Task:" checkbox to make the action use a background
thread. NetBeans will generate a searchWikipedia
method 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.

<br "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 "http://www.geonames.org/export/geonames-search.html">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&lt;WikiWaypoint&gt; doInBackground() {
    try {
        // example: http://ws.geonames.org/wikipediaSearch?q=london&amp;maxRows=10
        URL url = new URL("http://ws.geonames.org/wikipediaSearch?q="+
            jTextField1.getText()+"&amp;maxRows=10");

        XPath xpath = XPathFactory.newInstance().newXPath();
        NodeList list = (NodeList) xpath.evaluate("//entry", 
                new InputSource(url.openStream()),
                XPathConstants.NODESET);

        Set&lt;WikiWaypoint&gt; waypoints = new
            HashSet&lt;WikiMashupView.WikiWaypoint&gt;();
        for(int i = 0; i &lt; 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 entry
elements and then extract the title, lat,
and lng elements. For each entry the code creates a
WikiWaypoint, which is just a subclass of
Waypoint with an extra field to store the title of the
entry. All of this code is inside the doInBackground
method 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 of
WikiWaypoints. 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 custom
WaypointRenderer.


@Override protected void succeeded(Set&lt;WikiWaypoint&gt; 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.

<br "Figure 8. Searching for Java on Wikipedia" />
Figure 8. Searching for Java on Wikipedia

Conclusion

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 "http://www.google.com/apis/maps/">Google and "http://developer.yahoo.com/maps/">Yahoo to
see what other people are doing with mashups.

Now that you know how to build mashups in Swing using the
JXMapViewer, 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 "http://community.java.net/javadesktop/">JavaDesktop.org.
Mapping is a big part of the future, and desktop Java is right there
with it.

Resources


width="1" height="1" border="0" alt=" " />
Josh Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.
Related Topics >> GUI   |   Swing   |   

Comments

There are no words to express how AWESOME this article ...

There are no words to express how AWESOME this article is.
Dude, seriously, I'm really trying hard to find them, but this is definitely too awesome to be described with mere words.