Desktop Java

OpenMap Tutorial 4 – Layers

1. Introduction

In the first tutorial we created a basic OpenMap GIS application that displays a map with one shape layer, loaded from the filesystem, inside a JFrame. That tutorial was based on com.bbn.openmap.app.example.SimpleMap. In the second tutorial we extended our basic application to use the MapHandler and in the third tutorial we saw how it makes use of openmap.properties to wire everything together. In this tutorial we will talk about map layers.

2. Map Layers Overview

In Tutorial 1 we saw how to add a ShapeLayer into a MapBean. OpenMap supports a large number of layer types that can be added to a MapBean as shown in Figure 1.

The BufferedLayerMapBean is a MapBean that uses a BufferedLayer (see Figure 1) to create an image for some layers. It uses the Layer background flag to determine which layers should be added to this buffered image. Any layer that isn’t responding to MouseEvents should be designated as a background layer to reduce its paint() method processing load on the application.

The OMGraphicHandlerLayer is a base layer class that implements the most common functionality of a layer that needs to interact and share OMGraphics. The GraticuleLayer creates its OMGraphics internally, while the ShapeLayer reads data from a file. The DTEDLayer and RpfLayer have image caches. There are special layers that allow you to access a spatial database to create OMGraphics. Any technique of managing graphics can be used within a layer.

As a base class, the OMGraphicHandlerLayer has built-in capabilities that make it easier to manage OMGraphics in a layer. When extending the OMGraphicHandlerLayer, implement the prepare() method to return an OMGraphicList containing the OMGraphics appropriate for the projection currently set in the layer. All the work gathering, preparing and generating the OMGraphics should be performed in the prepare() method. The OMGraphicHandlerLayer also has a built-in SwingWorker object that can be used to call prepare() in a separate thread. The SwingWorker thread can be started by calling the doPrepare() method. If the SwingWorker is already busy when the doPrepare() method is called, a new thread will be launched to call prepare() when the original thread completes. In the default implementation of the prepare() method, the current list is simply generated with the current projection and returned.

Figure 1: OpenMap’s layers class hierarchy
Figure 1: OpenMap’s layers class hierarchy

Please refer to the Developer’s guide for more information on how to use these layers.

OpenMap also provides some layers for training (com.bbn.openmap.layer.learn):

  • BasicLayer
  • InteractionLayer
  • SimpleAnimationLayer
  • ProjectionResponsiveLayer

as well as test layers (com.bbn.openmap.layer.test):

  • BoundsTestLayer
  • GeoCrossDemoLayer
  • GeoIntersectionLayer
  • GeoTestLayer
  • HelloWorldLayer
  • TestLayer

Finally, the com.bbn.openmap.layer.DemoLayer is an example of how a layer can use the OMDrawingTool to edit OMGraphics. It uses the drawing tool to create areas on the map which are used as filters to control which of its OMGraphics are visible, too.

Layers derive from java.awt.Component and they are the only components that can be added to a MapBean. Because Layers are Components contained within a MapBean container, the rendering of Layers onto the map is controlled by the Java component rendering mechanism. This mechanism controls how layered components are painted on top of each other. To make sure that each component gets painted into the window in the proper order, the Component class includes a method that allows it to tell the rendering mechanism that it would like to be painted. This feature allows Layers to work independently from each other, and lets the MapBean avoid knowing what is happening on the Layers.

Layers in an OpenMap application can use data from many sources:

  • By computing them
  • From data files of a local hard drive
  • From data files from a URL
  • From data files contained in a jar file
  • Using information retrieved from a database (JDBC)
  • Using information received from a map server (images or map objects)

2.1 Creating layers

Let’s start with the simplest layers. Already from Tutorial 1 we saw how to create a ShapeLayer and add it into a MapBean. But since Tutorial 3 we are equipped with the power of openmap.properties and MapHandler. Here are the changes you need to do.

Listing 1 – openmap.properties

...
# These layers are turned on when the map is first started.  Order
# does not matter here...
openmap.startUpLayers=basic graticule shapePolitical
# Layers listed here appear on the Map in the order of their names.
openmap.layers=basic graticule shapePolitical

# Basic Layer
basic.class=com.bbn.openmap.layer.learn.BasicLayer
basic.prettyName=Basic
...

As a short reminder, openmap.layers references the layers to be loaded and openmap.startUpLayers references those that need to be loaded on startup. You will notice that basic layer has been added.

The result can be visualized in Figure 2. From BasicLayer’s JavaDoc we learn that it extends OMGraphicHandlerLayer, which contains a good bit of functionality, but exposes only the methods you need to start adding features (OMGraphics) on the map. This is a layer where the objects never change, and the map objects used by this layer never change. They always get managed and drawn, even if they are off the visible map. When the projection changes, the OMGraphics are told what the new projection is so they can reposition themselves, and then they are redrawn. If you want to learn more about interacting with your OMGraphics after you get the hang of displaying them efficiently, then move to the InteractionLayer.

Layers implement the ProjectionListener interface to listen for ProjectionEvents. When the projection changes, they may need to re-fetch, regenerate their graphics, and then repaint themselves into the new view. We shall say more things about projection in a future tutorial.

BasicLayer overrides two methods from OMGraphicHandlerLayer:

  • prepare(): This method gets called when the layer is added to the map, or when the map projection changes. We need to make sure the OMGraphicList returned from this method is what we want painted on the map. The OMGraphics need to be generated with the current projection. We test for a null OMGraphicList in the layer to see if we need to create the OMGraphics. This layer doesn’t change its OMGraphics for different projections, if your layer does, you need to clear out the OMGraphicList and add the OMGraphics you want for the current projection.
  • init(): Called from the prepare() method if the layer discovers that its OMGraphicList is null.

Figure 2: Basic layer
Figure 2: Basic layer

Listing 2 – prepare() method

public synchronized OMGraphicList prepare() {
      OMGraphicList list = getList();
      if (list == null) {
         list = init();
      }
      list.generate(getProjection());
      return list;
   }

getList() returns whatever was returned from this method the last time prepare() was called. In this example, we always return an OMGraphicList object, so if it’s null, prepare() must not have been called yet. In that case, init() is being called.

Before returning the list of map objects, a call to set the layer projection is critical! OMGraphics need to be told where to paint themselves, and they figure that out when they are given the current Projection in the generate(Projection) call. If an OMGraphic‘s location is changed, it will need to be regenerated before it is rendered, otherwise it won’t draw itself. You can have a generate problem when OMGraphics show up with the projection changes (zooms and pans), but not at any other time after something about the OMGraphic changes. If you want to be more efficient, you can replace this call to the list as an else clause to the (list == null) check above, and call generate(Projection) on all the OMGraphics in the init() method below as you create them. This will prevent the OMGraphicList.generate(Projection) call from making an additional loop through all of the OMGraphics before they are returned.

Listing 3 – init() method

public OMGraphicList init() {
      OMGraphicList omList = new OMGraphicList();

      // Add an OMLine
      OMLine line = new OMLine(40f, -145f, 42f, -70f, OMGraphic.LINETYPE_GREATCIRCLE);
      // line.addArrowHead(true);
      line.setStroke(new BasicStroke(2));
      line.setLinePaint(Color.red);
      line.putAttribute(OMGraphicConstants.LABEL, new OMTextLabeler("Line Label"));

      omList.add(line);

      // Add a list of OMPoints.
      OMGraphicList pointList = new OMGraphicList();
      for (int i = 0; i < 100; i++) {
         OMPoint point = new OMPoint((float) (Math.random() * 89f), (float) (Math.random() * -179f), 3);
         point.setFillPaint(Color.yellow);
         point.setOval(true);
         pointList.add(point);
      }
      omList.add(pointList);
      return omList;   
  }

The init() method is called when prepare() returns null. It creates the features (OMGraphics) to be added to the layer.

As a short tutorial, a typical GIS application consists of a map (the MapBean in OpenMap) that consists of layers (Layer objects) that consist of features (OMGraphics). The following figure shows the class hierarchy of OMGraphics.

Figure 3: OMGraphics class hierarchy
Figure 3: OMGraphics class hierarchy

The code of Listing 3 creates an OMLine and 100 OMPoints. The OMGraphics are raster and vector graphic objects that know how to position and render themselves on a given x-y window or lat-lon map projection. All you have to do is supply the location data (x/y, lat/lon) and drawing information (color, line width) and the graphic handles the rest.

This should be an easy and good start (we shall see e.g. how we can display data from a database in a following tutorial), but as you might have noticed, there is no interactivity. Interactivity is demonstrated by InteractionLayer. You should by now be able to replace Basic layer with InteractionLayer in openmap.properties.

InteractionLayer demonstrates how to interact with your OMGraphics on the map, getting them to change appearance with mouse events and provide additional information about themselves. This layer builds on the example demonstrated in the BasicLayer.

Figure 4: Interaction Layer
Figure 4: Interaction Layer

If you run the application, you will notice that when you move the mouse over an OMPoint, it changes colour. You may also right-click on it in order to display a popup menu. We shall use this functionality in order to display a feature’s properties. You may review com.bbn.openmap.layer.learn.InteractionLayer yourself. I just put down here some short guidelines on how to add interactions to your OMGraphicHandlerLayer. Don’t forget to add this line in the constructor:

Listing 4 – setMouseModeIDsForEvents()

// Making the setting so this layer receives events from the
// SelectMouseMode, which has a modeID of "Gestures". Other
// IDs can be added as needed. You need to tell the layer which
// MouseMode it should listen to, so it can tell the MouseModes to send
// events to it.
// Instead of "Gestures", you can also use SelectMouseMode.modeID or 
// OMMouseMode.modeID
setMouseModeIDsForEvents(new String[] { SelectMouseMode.modeID });  // "Gestures"         

This actually tells your layer that its features should respond to mouse gestures (e.g. mouse over). MouseEvents can be managed by certain OpenMap components, directing them to layers and to OMGraphics. MouseModes describe how MouseEvents and MouseMotionEvents are interpreted and consumed. The MouseDelegator is the real MouseListener and MouseMotionListener on the MapBean. The MouseDelegator manages a list of MouseModes, and knows which one is ‘active’ at any given time. The MouseDelegator also asks the active Layers for their MapMouseListeners, and adds the ones that are interested in events from the active MouseMode as listeners to that mode.

When a MouseEvent gets fired from the MapBean, it goes through the MouseDelegator to the active MouseMode, where the MouseMode starts providing the MouseEvent to its MapMouseListeners. Each listener is given the chance to consume the event. A MapMouseListener is free to act on an event and not consume it, so that it can continue to be passed on to other listeners.

The MapMouseListener provides a String array of all the MouseMode ID strings it is interested in receiving events from, and also has its own methods that the MouseEvents and MouseMotionEvents arrive in. The MapMouseListener can use these events, combined with the OMGraphicList, to find out if events have occurred over any OMGraphics, and respond if necessary.

There are a number of MouseModes that your layer can interact with. A search for modeID in the source code or here returns the following 8 mouse modes (there are two duplicates):

  • DistanceMouseMode.modeID = "Distance"
  • DistQuickToolMouseMode.modeID = "Distance"
  • NavMouseMode.modeID = "Navigation"
  • NullMouseMode.modeID = "None"
  • OMDrawingToolMouseMode.modeID = "Drawing"
  • OMMouseMode.modeID = "Gestures"
  • PanMouseMode.modeID = "Pan"
  • RangeRighsMouseMode.modeID = "RangeRings"
  • SelectMouseMode.modeID = "Gestures"
  • ZoomMouseMode.modeID = "Zoom"

Don’t forget to add something like the following to your features when you create them, for the visual display when the mouse goes over them: point.setSelectPaint(Color.yellow);

You may override the following methods:

  • isSelectable() – Query that an OMGraphic is selectable. You must return true to make it selectable.
  • isHighlightable() – Query that an OMGraphic can be highlighted when the mouse moves over it.
  • getInfoText() – to display text in the status bar when the mouse is over an OMGraphic
  • getToolTipTextFor() – to display a tooltip when the mouse is over an OMGraphic

E.g.

Listing 5 – methods for interaction

    /**
     * Query that an OMGraphic can be highlighted when the mouse moves over it.
     * If the answer is true, then highlight with this OMGraphics will be
     * called, and unhighlight will be called with the mouse is moved off of it.
     * 
     * @see com.bbn.openmap.layer.OMGraphicHandlerLayer#highlight
     * @see com.bbn.openmap.layer.OMGraphicHandlerLayer#unhighlight
     */
    public boolean isHighlightable(OMGraphic omg) {
        return true;
    }

    /**
     * Query that an OMGraphic is selectable. Examples of handing selection are
     * in the EditingLayer. The default OMGraphicHandlerLayer behavior is to add
     * the OMGraphic to an OMGraphicList called selectedList. If you aren't
     * going to be doing anything in particular with the selection, then return
     * false here to reduce the workload of the layer.
     * 
     * @see com.bbn.openmap.layer.OMGraphicHandlerLayer#select
     * @see com.bbn.openmap.layer.OMGraphicHandlerLayer#deselect
     */
    public boolean isSelectable(OMGraphic omg) {
        return true;
    }

If you want to display a popup menu when you right-click on an OMGraphic, override the following method to return a list of menu items. In a future tutorial we shall use this method to display properties of a feature from a database.

  • List getItemsForOMGraphicMenu(OMGraphic omg)

If you want to display a popup menu when you right-click anywhere on the layer, override the following method to return a list of menu items:

  • List getItemsForMapMenu(MapMouseEvent me)

This was quite nice and not that difficult, I hope, however you cannot move features around. E.g. I would like to be able to select an OMPoint and drag it to a new position. In order to be able to add this functionality we need to study some layers that are not listed in the above lists:

  • com.bbn.openmap.layer.DemoLayer
  • com.bbn.openmap.layer.DrawingToolLayer
  • com.bbn.openmap.layer.editor.EditorLayer

Try each one of them. We have actually seen DemoLayer in action in the previous tutorial but we didn’t look into the code. So, in order to be able to drag/modify features you need to:

  • Implement the DrawingToolRequestor interface
  • Define and initialize an instance of DrawingTool in findAndInit()
  • Modify isSelectable(), getInfoText(), getToolTipTextFor(), accordingly
  • Override select() and drawingComplete() methods as shown in Listing 4

Listing 6 – How to draw on the map

public class DemoLayer extends OMGraphicHandlerLayer implements DrawingToolRequestor {

   protected DrawingTool drawingTool;    

...

   public DrawingTool getDrawingTool() {
      // Usually set in the findAndInit() method.
      return drawingTool;
   }

   public void setDrawingTool(DrawingTool dt) {
      // Called by the findAndInit method.
      drawingTool = dt;
   }

   @Override 
   public void findAndInit(Object someObj) {
      if (someObj instanceof DrawingTool) {
         setDrawingTool((DrawingTool) someObj);
      }
   }

   @Override
   public void findAndUndo(Object someObj) {
      if (someObj instanceof DrawingTool) {
         if (getDrawingTool() == (DrawingTool) someObj) {
            setDrawingTool(null);
         }
      }
   }

   @Override
   public boolean isSelectable(OMGraphic omg) {
      DrawingTool dt = getDrawingTool();
      return (dt != null && dt.canEdit(omg.getClass()));
   }

   @Override
   public String getInfoText(OMGraphic omg) {
      DrawingTool dt = getDrawingTool();
      return (dt != null && dt.canEdit(omg.getClass())) ? "Click to edit graphic." : null;
   }

   @Override
   public String getToolTipTextFor(OMGraphic omg) {
      Object tt = omg.getAttribute(OMGraphic.TOOLTIP);
      if (tt instanceof String) {
         return (String) tt;
      }

      String classname = omg.getClass().getName();
      int lio = classname.lastIndexOf('.');
      if (lio != -1) {
         classname = classname.substring(lio + 1);
      }

      return "Your Layer Object: " + classname;
   }

   @Override
   public void select(OMGraphicList list) {
      if (list != null && !list.isEmpty()) {
         OMGraphic omg = list.getOMGraphicAt(0);
         DrawingTool dt = getDrawingTool();

         if (dt != null && dt.canEdit(omg.getClass())) {
            dt.setBehaviorMask(OMDrawingTool.QUICK_CHANGE_BEHAVIOR_MASK);
            if (dt.edit(omg, this) == null) {
               // Shouldn't see this because we checked, but ...
               fireRequestInfoLine("Can't figure out how to modify this object.");
            }
         }
      }
   }

   @Override
   public void drawingComplete(OMGraphic omg, OMAction action) {
      if (!doAction(omg, action)) {
         // null OMGraphicList on failure, should only occur if
         // OMGraphic is added to layer before it's ever been
         // on the map.
         setList(new OMGraphicList());
         doAction(omg, action);
      }
      repaint();
   }

...
}

When you run the application, you are now able to select and drag a feature to a new location, or modify its geometry (for lines, circles etc.).

When you click on the Drawing Tool Launcher button you are able to add many types of graphics on the layer, which might not be what you want. E.g. you might want that your layer only displays OMPoints and you don’t want the user to be able to add e.g. lines or circles to it by using the Drawing Tool. This can be easily done if you modify openmap.components to leave only omdrawingtool and ompointloader (or only the types of OM loaders you use in your application):

Listing 7 – openmap.components to not display the Drawing Tool (omdt)

openmap.components=menulist informationDelegator projFactory projectionstack toolBar zoompanel navpanel scalepanel projectionstacktool addlayer layersPanel overviewMapHandler layerHandler mouseDelegator projkeys coordFormatterHandler mouseModePanel mouseMode selectMouseMode navMouseMode distanceMouseMode panMouseMode omdrawingtool ompointloader

Another problem is that when you right-click on an OMPoint, a different popup menu appears than the one you created via getItemsForOMGraphicMenu().

The culprit is OMDrawingTool. Since OpenMap is an open source project, you are encouraged to read the code of the above class (com.bbn.openmap.tools.drawing.OMDrawingTool). Actually, in method select() you will see the line:

dt.setBehaviorMask(OMDrawingTool.QUICK_CHANGE_BEHAVIOR_MASK);

OMDrawingTool defines a number of behaviour masks:

  • SHOW_GUI_BEHAVIOR_MASK
  • GUI_VIA_POPUP_BEHAVIOR_MASK
  • USE_POPUP_BEHAVIOR_MASK
  • ALT_POPUP_BEHAVIOR_MASK
  • PASSIVE_MOUSE_EVENT_BEHAVIOR_MASK
  • DEACTIVATE_ASAP_BEHAVIOR_MASK
  • DEFAULT_BEHAVIOR_MASK
  • QUICK_CHANGE_BEHAVIOR_MASK

You may try all of them to see how the layer behaves. Unfortunately, none of them covers our needs. If no popup (or our popup) appears, the feature cannot be dragged to another location; if a popup appears it the one of the OMDrawingTool. So, go a hack. We will create our own OMDrawingTool:

Listing 8 – MyDrawingTool class

public class MyDrawingTool extends OMDrawingTool {

    public MyDrawingTool() {
        super();
        setBehaviorMask(OMDrawingTool.QUICK_CHANGE_BEHAVIOR_MASK);
    }
    
    @Override
    public JPopupMenu createPopupMenu() {
        JPopupMenu popup =  super.createPopupMenu(); 
        popup.removeAll();
        popup.add(new JMenuItem("Which"));
        popup.add(new JMenuItem("Why"));
        return popup;
    }
}

You could go even further adding set methods to set a list of JMenuItems created by getItemsForOMGraphicMenu() method, but I leave this as an exercise to you. We need to do one more thing:

Listing 9 – openmap.properties to display our Drawing Tool

omdrawingtool.class=openmap.MyDrawingTool
#omdrawingtool.class=com.bbn.openmap.tools.drawing.OMDrawingTool

With this change you can delete the line that sets the behaviour mask in your layer’s select() method.

OpenMap layers support animation, too. Replace the previous layer with SimpleAnimationLayer in openmap.properties. When you re-run the application again, you see an empty map. Click on the layers button and select the AnimationLayer’s properties (see Figure 5). In the dialog box that appears, add sprites by clicking on the respective button, and once you are happy, check the Run Timer check box to see them moving. You may adjust the Timer interval slider to see them moving faster or slower.

Figure 5: Animation Layer
Figure 5: Animation Layer

All the previously mentioned layers extend OMGraphicHandlerLayer. But you can do without it. For example, take a look at HelloWorldLayer which overrides Layer directly. Its createGraphics() method creates features and adds them to the passed OMGraphicList.

You are encouraged to check the other layers mentioned in the beginning of this article, like the TestLayer, GeoTestLayer etc.

To make the Properties button enabled in the Layers dialog box and be able to display something, you need to override the getGUI() method. See e.g. TestLayer or SimpleAnimationLayer.

3. Conclusion

This tutorial was devoted to OpenMap’s layers. We started from the simple BasicLayer, which displays static data, then added interaction with the InteractionLayer, demonstrated how to move features to new map locations or modify the geometry of features with the mouse, and continued with AnimationLayer to demonstrate how to animate a layer’s features. We didn’t cover everything. Even though saw how to add and manipulate features, we didn’t talk about projections, yet. In the next tutorial we will build our first 3-tier application where we shall see how to display data from a database on the map.

Ioannis Kostaras

Software architect awarded the 2012 Duke's Choice Community Choice Award and co-organizing the hottest Java conference on earth, JCrete.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
George
George
8 years ago

Could you also post the code of the openmap.properties?
I don’t know what I am missing, but I don’t get the tool tips nor the infoText when I go over a graphic with the InteractionLayer, or any other layer.

Scott
Scott
7 years ago

Are you still working on a 5th tutorial?
Your tutorials have been of great value and I highly appreciate the work you’ve put into them.
In the mean time, do you have any recommendations of where to look to get a demonstration of how to display data from a database? Any good examples within the OpenMap code itself? This is a part I am stuck at now.

Igor
Igor
6 years ago

This is a really great tutorial. Helped me a lot to understand how to work with OpenMap.
One thing I am struggling with is trying to add DTED layer for showing relief on the map (my understanding is this is what DTED layer is for). Is there a way to provide a brief basic example?
Thank you so much!

Back to top button