Step 1 Additional apis

Moving right on we will need a couple of extra APIs.

The first is for the display of the map. I will be using Piccolo2D (http://piccolo2d.org/index.html), which is a “toolkit that supports the development of 2D structured graphics programs, in general, and Zoomable User Interfaces (ZUIs), in particular”.

The second api is a dockable framework for swing (http://dock.javaforge.com/). “DockingFrames is an open source Java Swing docking framework, licenced under LGPL 2.1.

Most graphical user interfaces consist of some panels. A JTree showing some files on the left, a JTable showing detailed information about a directory on the right, … DockingFrames provides a way to organize these panels such that the user can drag & drop them.”

Add these dependencies to the pom.xml:

		
        <dependency>
            <groupId>org.piccolo2d</groupId>
            <artifactId>piccolo2d-core</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.piccolo2d</groupId>
            <artifactId>piccolo2d-extras</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.dockingframes</groupId>
            <artifactId>docking-frames-common</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.dockingframes</groupId>
            <artifactId>docking-frames-core</artifactId>
            <version>1.1.1</version>
        </dependency>

Step 2 Changes to the parser

As this early version of the map viewer we are creating will not persist any of the map data in a spatial database we are obliged to store the data in memory in some form. To this end we will create an OsmMap class which will wrap the following two variables:

private HashMap&lt;Long, Node&gt; nodes = new HashMap&lt;Long, Node&gt;();
private ArrayList ways = new ArrayList();

Update the parser class as follows:

    public OsmosisParser(File file, final OsmMap map) throws FileNotFoundException {
 
        Sink sinkImplementation = new Sink() {
            public void process(EntityContainer entityContainer) {
                Entity entity = entityContainer.getEntity();
                if (entity instanceof Node) {
                    Node n = (Node)  entity;
                    map.getNodes().put(n.getId(), n);
                    numNodes++;
                } else if (entity instanceof Way) {
                    Way w = (Way)entity;
                    map.getWays().add(w);
                    numWays++;
                } else if (entity instanceof Relation) {
                    numRelations++;
                }
            }
 
            public void release() {
            }
 
            public void complete() {
            }
 
            public void initialize(Map&lt;String, Object&gt; arg0) {
 
            }
        };

As the parser reads the data it will add the nodes and ways into our map wrapper.

Step 3. Features

In this tutorial we will display 3 types of map data:

  1. Roads
  2. Forests
  3. Buildings

There are many types of feature and sub-feature in the Open Street Map data but these 3 are a good start for displaying a map. Extra features can be added later. The features available can be found on the Open Street Maps site here (http://wiki.openstreetmap.org/wiki/Map_Features).

Features are broken down into 3 broad types: images, lines and areas. Buildings and forests are areas and higways are lines. As can be seen there are many sub-types of the Buildings feature. In this part of the tutorial no difference will be made between the types of building.

Forest is different in that it is a sub-type of the “landuse” feature. There are many types of highway, but I will only draw motorways, trunk roads, primary, secondary and tertiary sub-types.

Step 4. Create simple swing app

In this step we will create a simple swing frame with two dockable windows. One will contain the map and the other a tree view with the different layers. The different layers can be made visible using check boxes rendered into the tree nodes.

Create 4 new classes:

We will use a hierarchical enum for modelling the features: Feature.

public enum Feature {
 
    Feature(null),
 
    building(Feature),
 
    landuse(Feature), 
    forest(landuse),
 
    highway(Feature), 
    motorway(highway), 
    motorway_link(highway), 
    trunk(highway), 
    trunk_link(highway), 
    primary(highway), 
    primary_link(highway), 
    secondary(highway), 
    tertiary(highway),
 
    unknown(Feature);
 
    private Feature parent = null;
 
    /**
     * Initialises a feature
     * 
     * @param parent
     */
    private Feature(Feature parent) {
        this.parent = parent;
        if (this.parent != null) {
            this.parent.children.add(this);
        }
    }
 
    private List&lt;Feature&gt; children = new ArrayList&lt;Feature&gt;();
...

In the above code snippet you can see how the hierarchy is created.

The features are rendered one at a time recursively:

    private void processFile(File file) throws FileNotFoundException {
 
        new OsmosisPbfParser(file, theMap);
 
        renderer.initialise();
        for (Feature f : Feature.Feature.children()) {
            renderFeature(f);
        }
    }
 
    private void renderFeature(Feature f) {
        renderer.renderLayer(f, theMap);
        for (Feature sf: f.children()){
            renderFeature(sf);
        }
    }

When it comes to the rendering there is one important thing to take into account; the odes in the Osm map data files are in latitudes and longitudes. These need to be converted to x and y to “flatten” out the curved nature of the earth. This is done using a Mercator projection:

private double lat2y(double latitude) {
    return Math.toDegrees(Math.log(Math.tan(Math.PI / 4 + Math.toRadians(latitude) / 2)));
}
 
private double lon2x(double longitude) {
    return longitude;
}

When you look through the code you will notice the following oddity:

double x = lon2x(longitude) * 100;
double y = -lat2y(latitude) * 100;

The * 100 is just a scale tweak, but the “-lat2y(…” is because Piccolo2D (being Java2D based) is has the origin in the top left corner rather than the bottom left corner – thus the need to mirror the drawings

I have also noticed (through trial and null pointer error), that some of the data is not quite a number (NaN) and so I have to check for this:

if (Double.isNaN(x) | Double.isNaN(y)) {
   error = true;
   break;
}

What else? Oh yes rendering the map. Piccolo2D uses layers (PLayer) which are a sub-class of PNode. For each different Feature I have created a PLayer and added to its parent Feature PLayer. All the features are then drawn on this PLayer – either as filled in areas or Polylines.

For PolyLines:

        if (!error) {
            PPath path = PPath.createPolyline(points);
            path.setStroke(new BasicStroke(0.01f));
            path.setStrokePaint(fillColor);
            path.setPaint(null);
            subLayer.addChild(path);
        }

For Areas:

        if (!error) {
            PPath path = PPath.createPolyline(points);
            path.setStroke(new BasicStroke(0.0001f));
            path.setPaint(fillColor);
            path.setStrokePaint(edgeColor);
            layer.addChild(path);
        }

When a check box in the options panel is ticked or un-ticked then the relevant PLayer is set to be visible or not. All the PNodes andPLayers that are attached to the PLayer in question are then made visible or hidden accordingly.

02-01

Here is a screen shot of the finished application zoomed in to Luxembourg town centre:

Conclusion

So there you have it the basic display of the Luxembourg map. I have not gone into any real detail on the options panel as it is just some standard java with a cell renderer and cell editor for the check boxes.

Adding features is a matter of adding it in the Feature enum class and updating the Renderer class to select the right colours.

 

Drawing some simple features