Drawing a line on a GeoTools Map

As anyone following the dismal news out of the UK recently will know we had some sort of royal extravaganza down south in England this weekend, so with nothing better to do (and an extra day off) I decided to do some playing with GeoTools. This was mostly motivated by the work I had already done in answering this gis.stackexchange.com question about how to draw a line on top of a map. It seems that there isn’t much about how to do this in the GeoTools documentation.

So I scratched my head and dredged up what I could remember about swing and particularly about the GeoTools swing module and came up with some code that answered the immediate question which was why the OP got many layers in the map rather than one. I decided that this might be useful for other people so I tidied the code up and created a small project that contains a DigitizerAction and a Digitizer class which is the actual tool. It’s pretty simple all it does is place a series of dots on the screen and then generates a LineString which is added to a list, which is then used to generate a new FeatureLayer and removes the old layer (if it exists).

I finished up by adding a little demo program which adds a draw button to the toolbar and displays the US States for you to draw over.

A screenshot of the demo

All you do is click the draw button, then each click on the map will add a point to the current line, a double click finishes the line and forces a redraw of the screen with the line now in red.

Details

For anyone who’s trying to create a new tool for their swing application with GeoTools here is a little more on how it works.

Action

We need to create an Action to tell swing what we plan to do, I called mine DigitizerAction and made it extend MapAction which Michael Bedward (the original author of the gt-swing module) helpfully provided to save us typing (or pasting) in a lot of boiler plate code. All I have to provide is some code to initialise the action’s icon, name etc and an actionPerformed method to set the tool up to actually do something when the button is clicked.

Tool

Again to save time and effort the Digitizer tool extends CursorTool which extends MapMouseAdapter so we don’t need to worry about how to listen to the mouse’s movements or how to get a real world position from a mouse click. Much of the code is either set up or book keeping. For set up we need to generate a FeatureType for the Features we’ll be building later, and a style so they show up on the map (a more advanced tool might let the user override that default style). The only other thing to take care of in the constructor is setting the cursor to a simple cross hair.

   public Digitizer() {
        SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder();
        b.setName("LineFeature");
        b.add("line", LineString.class);
        SimpleFeatureType TYPE = b.buildFeatureType();
        featureBuilder = new SimpleFeatureBuilder(TYPE);
        geometryFactory = JTSFactoryFinder.getGeometryFactory(JTSFactoryFinder.EMPTY_HINTS);
        style = SLD.createLineStyle(Color.red, 2.0f);

        ImageIcon imgIcon = new ImageIcon(getClass().getResource(ICON_IMAGE));
        cursor = new Cursor(Cursor.CROSSHAIR_CURSOR);
    }

The class also has some fields that we’ll need later lastX and lastY which is the position of the last click, an ArrayList of Coordinates which are the real world positions of the current line, and a list of SimpleFeatures which hold the lines that we have already drawn. We need to keep track of the previous features as the display layer is recreated every time we add a line.

The actual interaction with the user all occurs inside the onMouseClicked method which is called each time a mouse button is clicked.

    public void onMouseClicked(MapMouseEvent e) {

        if (e.getClickCount() > 1) { // was it a double click
            drawTheLine(positions);
            first = true;
        } else { // add a new point
            DirectPosition2D pos = e.getWorldPos();

            positions.add(new Coordinate(pos.x, pos.y));
            // Put a marker at each digitized point
            Graphics graphics = (Graphics2D) ((JComponent) getMapPane()).getGraphics().create();
            int x = e.getX();
            int y = e.getY();
            if (!first) {
                graphics.drawLine(lastX, lastY, x, y);
            }
            first = false;
            lastX = x;
            lastY = y;
            graphics.fillRect(x - 3, y - 3, 6, 6);
        }
    }

Here we first check if the user has clicked twice within the system time limit (so it is a double click), if it is we call the drawTheLine method on the positions list to add the line to the screen, we’ll look at that in a moment. We also reset the first flag to say that the next click (if there is one) is the first in a line.

If the user only clicked once (or this is the first click of a double click) we will go through the else branch, where we get the world position of the click and add that to our list of coordinates. We then grab a graphics from our map pane to draw a temporary mark so the user knows we’re listening and have seen their click, here we need the X and Y pixel coordinates. If this is not the first point in the line we draw a line from the last point (lastX, lastY) to the current point (x, y) and then we draw a point at the current point (x, y). We also make a note of this point for next click to be the start of the line and note that we now have previous point by making first false.

The last remaining step is to draw lines on the map as SimpleFeatures, this is done in drawTheLine where we generate a new LineString using the GeometryFactory, we then add that LineString to the FeatureBuilder and create a new SimpleFeature from it. Note that we leave the id set to null so it generates a new FID for each feature. The feature is then stored in the features list, the existing layer is removed from the map (to prevent us ending up with an ever increasing number of map layers). Then we create a new FeatureLayer using the DataUtilities.collection method to convert our list of SimpleFeatures to a SimpleFeatureCollection and applying the style we made earlier. Finally, we reset the positions list to empty to be ready to store the next line for the user.

Usage

If you want to use this in your own map you can simply do the following:

    mapFrame = new JMapFrame();
    mapFrame.enableToolBar(true);
    JToolBar toolBar = mapFrame.getToolBar();
    DigitizerAction d = new DigitizerAction(mapFrame.getMapPane());
    JButton btn = new JButton(d);
    toolBar.addSeparator();
    toolBar.add(btn);

You can then get the list of features by using d.getFeatures() if you want to provide a save function or to send them to some other store.