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 MouseEvent
s 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 OMGraphic
s. The GraticuleLayer
creates its OMGraphic
s 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 OMGraphic
s. 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 OMGraphic
s in a layer. When extending the OMGraphicHandlerLayer
, implement the prepare()
method to return an OMGraphicList
containing the OMGraphic
s appropriate for the projection currently set in the layer. All the work gathering, preparing and generating the OMGraphic
s 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.
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 OMGraphic
s. It uses the drawing tool to create areas on the map which are used as filters to control which of its OMGraphic
s are visible, too.
Layers derive from java.awt.Component
and they are the only components that can be added to a MapBean
. Because Layer
s are Component
s contained within a MapBean
container, the rendering of Layer
s 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 Layer
s.
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 (OMGraphic
s) 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 OMGraphic
s 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 OMGraphic
s after you get the hang of displaying them efficiently, then move to the InteractionLayer
.
Layers implement the ProjectionListener
interface to listen for ProjectionEvent
s. 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 theOMGraphicList
returned from this method is what we want painted on the map. TheOMGraphic
s need to be generated with the current projection. We test for a nullOMGraphicList
in the layer to see if we need to create theOMGraphic
s. This layer doesn’t change itsOMGraphic
s for different projections, if your layer does, you need to clear out theOMGraphicList
and add theOMGraphics
you want for the current projection.init()
: Called from theprepare()
method if the layer discovers that itsOMGraphicList
isnull
.
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! OMGraphic
s 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 OMGraphic
s 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 OMGraphic
s before they are returned.
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 (OMGraphic
s) 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 (OMGraphic
s). The following figure shows the class hierarchy of OMGraphic
s.
The code of Listing 3 creates an OMLine
and 100 OMPoint
s. The OMGraphic
s 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 OMGraphic
s 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
.
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). MouseEvent
s can be managed by certain OpenMap components, directing them to layers and to OMGraphic
s. MouseMode
s describe how MouseEvent
s and MouseMotionEvent
s are interpreted and consumed. The MouseDelegator
is the real MouseListener
and MouseMotionListener
on the MapBean
. The MouseDelegator
manages a list of MouseMode
s, and knows which one is ‘active’ at any given time. The MouseDelegator
also asks the active Layers for their MapMouseListener
s, 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 MapMouseListener
s. 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 MouseEvent
s and MouseMotionEvent
s arrive in. The MapMouseListener
can use these events, combined with the OMGraphicList
, to find out if events have occurred over any OMGraphic
s, 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 anOMGraphic
is selectable. You must returntrue
to make it selectable.isHighlightable()
– Query that anOMGraphic
can be highlighted when the mouse moves over it.getInfoText()
– to display text in the status bar when the mouse is over anOMGraphic
getToolTipTextFor()
– to display a tooltip when the mouse is over anOMGraphic
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
infindAndInit()
- Modify
isSelectable()
,getInfoText()
,getToolTipTextFor()
, accordingly - Override
select()
anddrawingComplete()
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 OMPoint
s 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 JMenuItem
s 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.
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.
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.
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.
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!