// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/OMGraphicHandlerLayer.java,v $
// $RCSfile: OMGraphicHandlerLayer.java,v $
// $Revision: 1.33 $
// $Date: 2007/04/24 19:53:44 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.bbn.openmap.Layer;
import com.bbn.openmap.PropertyConsumer;
import com.bbn.openmap.event.InfoDisplayEvent;
import com.bbn.openmap.event.LayerStatusEvent;
import com.bbn.openmap.event.MapMouseEvent;
import com.bbn.openmap.event.MapMouseListener;
import com.bbn.openmap.event.ProjectionEvent;
import com.bbn.openmap.layer.policy.ProjectionChangePolicy;
import com.bbn.openmap.layer.policy.RenderPolicy;
import com.bbn.openmap.layer.policy.StandardPCPolicy;
import com.bbn.openmap.layer.policy.StandardRenderPolicy;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.FilterSupport;
import com.bbn.openmap.omGraphics.OMAction;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicHandler;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.event.GestureResponsePolicy;
import com.bbn.openmap.omGraphics.event.MapMouseInterpreter;
import com.bbn.openmap.omGraphics.event.StandardMapMouseInterpreter;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.tools.icon.IconPart;
import com.bbn.openmap.tools.icon.OMIconFactory;
import com.bbn.openmap.tools.icon.OpenMapAppPartCollection;
import com.bbn.openmap.util.ComponentFactory;
import com.bbn.openmap.util.ISwingWorker;
import com.bbn.openmap.util.PooledSwingWorker;
import com.bbn.openmap.util.PropUtils;
/**
* The OMGraphicHandlerLayer is a layer that provides OMGraphicHandler support.
* With this support, the OMGraphicHandlerLayer can accept OMAction instructions
* for managing OMGraphics, and can perform display filtering as supported by
* the FilterSupport object.
* <P>
*
* When extending this class for a simple layer, they only method you need to
* override is the prepare() method. This is a good class to use to start
* writing your own layers. Start with overriding the prepare() method, having
* it return an OMGraphicList containing OMGraphics on the map that are
* appropriate for the current projection.
* <P>
*
* The OMGraphicHandlerLayer already has an OMGraphicList variable, so if you
* extend this class you don't have to manage another one. You can add your
* OMGraphics to the list provided with getList(). If you create a list of
* OMGraphics that is reused and simply re-projected when the projection
* changes, do nothing - that's what happens anyway based on the default
* ProjectionChangePolicy set for the layer (StandardPCPolicy). You can either
* create an OMGraphicList in the constructor and set it by calling
* setList(OMGraphicList), or you can test for a null OMGraphicList returned
* from getList() in prepare() and create one if it needs to be. If the list
* isn't null, make sure you still call generate on it. The advantage of waiting
* to create the list in prepare is that the processing time to create the
* OMGraphics is delayed until the layer is added to the map. If you create
* OMGraphics in the constructor, you delay the entire program (maybe startup of
* the map!) while the OMGraphics are created.
* <P>
*
* If you let prepare() create a new OMGraphicList based on the new projection,
* then make sure the ProjectionChangePolicy for the layer is set to a
* com.bbn.openmap.layer.policy.ResetListPCPolicy, or at least clear out the old
* graphics at some point before adding new OMGraphics to the list in that
* method. You just have to do one, not both, of those things. If you are
* managing a lot of OMGraphics and do not null out the list, you may see your
* layer appear to lag behind the projection changes. That's because another
* layer with less work to do finishes and calls repaint, and since your list is
* still set with OMGraphics ready for the old projection, it will just draw
* what it had, and then draw again when it has finished working. Nulling out
* the list will prevent your layer from drawing anything on the new projection
* until it is ready.
* <P>
*
* The OMGraphicHandlerLayer has support built in for launching a SwingWorker to
* do work for you in a separate thread. This behavior is controlled by the
* ProjectionChangePolicy that is set for the layer. Both the StandardPCPolicy
* and ListResetPCPolicy launch threads by calling doPrepare() on this layer.
* The StandardPCPolicy only calls this if the number of OMGraphics on its list
* is greater than some cutoff value.
* <P>
*
* useLayerWorker variable is true (default), then doPrepare() will be called
* when a new ProjectionEvent is received in the projectionChanged method. This
* will cause prepare() to be called in a separate thread. You can use prepare()
* to create OMGraphics, the projection will have been set in the layer and is
* available via getProjection(). You should generate() the OMGraphics in
* prepare. NOTE: You can override the projectionChanged() method to
* create/manage OMGraphics any way you want. The SwingWorker only gets launched
* if doPrepare() gets called.
* <P>
*
* MouseEvents are not handled by a MapMouseInterpreter, with the layer being
* the GestureResponsePolicy object dictating how events are responded to. The
* interpreter does the work of fielding MapMouseEvents, figuring out if they
* concern an OMGraphic, and asking the policy what it should do in certain
* situations, including providing tooltips, information, or opportunities to
* edit OMGraphics. The mouseModes property can be set to the MapMouseMode IDs
* that the interpreter should respond to.
* <P>
*
* For OMGraphicHandlerLayers, there are several properties that can be set that
* dictate important behavior:
*
* <pre>
*
*
*
* layer.projectionChangePolicy=pcp
* layer.pcp.class=com.bbn.openmap.layer.policy.StandardPCPolicy
*
* layer.renderPolicy=srp
* layer.srp.class=com.bbn.openmap.layer.policy.StandardRenderPolicy
* # or
* layer.renderPolicy=ta
* layer.ta.class=com.bbn.openmap.layer.policy.RenderingHintsRenderPolicy
* layer.ta.renderingHints=KEY_TEXT_ANTIALIASING
* layer.ta.KEY_TEXT_ANTIALIASING=VALUE_TEXT_ANTIALIAS_ON
*
* layer.mouseModes=Gestures
* layer.consumeEvents=true
*
*
* </pre>
*/
public class OMGraphicHandlerLayer extends Layer implements GestureResponsePolicy, OMGraphicHandler {
public static Logger logger = Logger.getLogger("com.bbn.openmap.layer.OMGraphicHandlerLayer");
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* The property that can be set for the ProjectionChangePolicy. This
* property should be set with a scoping marker name used to define a policy
* class and any other properties that the policy should use.
* "projectionChangePolicy"
*
* @see com.bbn.openmap.layer.policy.ProjectionChangePolicy
* @see com.bbn.openmap.layer.policy.StandardPCPolicy
* @see com.bbn.openmap.layer.policy.ListResetPCPolicy
*/
public final static String ProjectionChangePolicyProperty = "projectionChangePolicy";
/**
* The property that can be set for the RenderPolicy. This property should
* be set with a marker name used to define a policy class and any other
* properties that the policy should use. "renderPolicy"
*
* @see com.bbn.openmap.layer.policy.StandardRenderPolicy
* @see com.bbn.openmap.layer.policy.BufferedImageRenderPolicy
* @see com.bbn.openmap.layer.policy.RenderingHintsRenderPolicy
*/
public final static String RenderPolicyProperty = "renderPolicy";
/**
* The property that can be set to tell the layer which mouse modes to
* listen to. The property should be a space-separated list of mouse mode
* IDs, which can be specified for a MapMouseMode in the properties file or,
* if none is specified, the default ID hard-coded into the MapMouseMode.
* "mouseModes"
*/
public final static String MouseModesProperty = "mouseModes";
/**
* The property that can be set to tell the layer to consume mouse events.
* The maim reason not to do this is in case you have OMGraphics that you
* are moving, and you need other layers to respond to let you know when you
* are over the place you think you need to be.
*/
public final static String ConsumeEventsProperty = "consumeEvents";
/**
* The property to tell the layer how transparent it is. 0 is totally clear,
* 1f is opaque.
*/
public final static String TransparencyProperty = "transparency";
/**
* The property to tell the layer if the thread launched for prepare()
* method calls can be interrupted. If false, the thread will be allowed to
* complete it's work. This (false) is generally a good setting for layers
* contacting servers. The default setting is, however, true.
*/
public final static String InterruptableProperty = "interruptable";
/**
* Filter support that can be used to manage OMGraphics.
*/
protected FilterSupport filter = new FilterSupport();
/**
* The ProjectionChangePolicy object that determines how a layer reacts and
* sets up the OMGraphicList to be rendered for the layer when the
* projection changes.
*/
protected ProjectionChangePolicy projectionChangePolicy = null;
/**
* The RenderPolicy object that determines how a layer's OMGraphicList is
* rendered in the layer.paint() method.
*/
protected RenderPolicy renderPolicy = null;
/**
* A SwingWorker that can be used for gathering OMGraphics or doing other
* work in a different thread.
*/
protected ISwingWorker<OMGraphicList> layerWorker;
/**
* A SwingWorker created if the projection changes when the primary
* layerworker is off doing work. If a bunch of project changes occur before
* the primary layerworker returns from being interrupted, setting the one
* in the queue will take care of all of them.
*/
protected boolean layerWorkerQueue = false;
protected String[] mouseModeIDs = null;
/**
* A flag to tell the layer to be selfish about consuming MouseEvents it
* receives. If set to true, it will consume events so that other layers
* will not receive the events. If false, lower layers will also receive
* events, which will let them react too. Intended to let other layers
* provide information about what the mouse is over when editing.
*/
protected boolean consumeEvents = false;
/**
* Flag used to avoid the SwingWorker to be interrupted. Useful for layers
* that load an image from a server such as the WMSPlugin to avoid an ugly
* java output "Interrupted while loading image".
*/
protected boolean interruptable = true;
/**
* Sets the interruptible flag, allowing the current swing worker thread to
* have interrupt called on it.
*/
public void setInterruptable(boolean b) {
interruptable = b;
}
/**
* Queries for the interruptible flag.
*
* @return true if interruptible flag is set, allowing interrupt to be
* called on swing worker threads.
*/
public boolean isInterruptable() {
return interruptable;
}
// OMGraphicHandler methods, deferred to FilterSupport...
/**
* Sets all the OMGraphics outside of this shape to be invisible. Also
* returns another OMGraphicList containing OMGraphics that are contained
* within the Shape provided.
*/
public OMGraphicList filter(Shape withinThisShape) {
return filter.filter(withinThisShape);
}
/**
* @see com.bbn.openmap.omGraphics.OMGraphicHandler#filter(Shape, boolean).
*/
public OMGraphicList filter(Shape shapeBoundary, boolean getInsideBoundary) {
return filter.filter(shapeBoundary, getInsideBoundary);
}
/**
* To find out whether SQL queries are handled.
*
* @see com.bbn.openmap.omGraphics.OMGraphicHandler#supportsSQL().
*/
public boolean supportsSQL() {
return filter.supportsSQL();
}
/**
* Depending on the filter's SQL support, returns an OMGraphicList that fit
* the query.
*/
public OMGraphicList filter(String SQLQuery) {
return filter.filter(SQLQuery);
}
/**
* Perform the OMAction on the OMGraphic, within the OMGraphicList contained
* in the layer.
*/
public boolean doAction(OMGraphic graphic, OMAction action) {
return filter.doAction(graphic, action);
}
/**
* Get the OMGraphicList held by the layer. May be null.
*/
public OMGraphicList getList() {
return filter.getList();
}
/**
* Indicates if the OMGraphicHandler can have its OMGraphicList set.
*/
public boolean canSetList() {
return filter.canSetList();
}
/**
* Set the OMGraphicList within this OMGraphicHandler. Works if
* canSetGraphicList == true.
*/
public void setList(OMGraphicList omgl) {
filter.setList(omgl);
}
/**
* Remove all filters, and reset all graphics to be visible.
*/
public void resetFiltering() {
filter.resetFiltering();
}
/**
* Don't set to null. This is here to let subclasses put a more/less capable
* FilterSupport in place.
*/
public void setFilter(FilterSupport fs) {
filter = fs;
}
/**
* Get the FilterSupport object that is handling the OMGraphicHandler
* methods.
*/
public FilterSupport getFilter() {
return filter;
}
/**
* From the ProjectionListener interface. The method gets called when the
* layer is part of the map, and whenever the map projection changes. Will
* trigger a repaint().
* <p>
*
* The ProjectionEvent is passed to the current ProjectionChangePolicy
* object, which determines what will happen on the layer and how. By
* default, a StandardPCPolicy is notified with the projection change, and
* it will test the projection for changes and make sure prepare() is
* called. It will make the decision whether doPrepare() is called, based on
* the number of OMGraphics on the list, which may launch a swing worker
* thread to call prepare(). The StandardPCPolicy does not do anything to
* the OMGraphicList when the projection changes.
* <p>
*
* If you need the OMGraphicList cleared out with a new projection, you can
* substitute a ListRestPCPolicy for the StandardPCPolicy. You would want to
* do this if your OMGraphicList changes for different projections - The
* reason the OMGraphicList is nulled out is so if another layer finishes
* before yours does and gets repainted, your old OMGraphics don't get
* painted along side their new ones - it's a mismatched situation. You can
* set the ProjectionChangePolicy directly with the
* setProjectionChangePolicy, or by overriding the getProjectionChangePolicy
* method and returning the type you want by default if it is null.
*
* @see com.bbn.openmap.layer.policy.ProjectionChangePolicy
* @see com.bbn.openmap.layer.policy.StandardPCPolicy
* @see com.bbn.openmap.layer.policy.ListResetPCPolicy
*/
public void projectionChanged(ProjectionEvent pe) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("OMGraphicHandlerLayer " + getName() + " projection changed, calling "
+ getProjectionChangePolicy().getClass().getName());
}
getRenderPolicy().prePrepare(pe.getProjection());
getProjectionChangePolicy().projectionChanged(pe);
}
/**
* Get the ProjectionChangePolicy that determines how a layer reacts and
* gathers OMGraphics for a projection change.
*/
public ProjectionChangePolicy getProjectionChangePolicy() {
if (projectionChangePolicy == null) {
projectionChangePolicy = new StandardPCPolicy(this);
}
return projectionChangePolicy;
}
/**
* Set the ProjectionChangePolicy that determines how a layer reacts and
* gathers OMGraphics for a projection change.
*/
public void setProjectionChangePolicy(ProjectionChangePolicy pcp) {
projectionChangePolicy = pcp;
// Just to make sure,
pcp.setLayer(this);
}
/**
* Get the RenderPolicy that determines how an OMGraphicList is rendered.
*/
public RenderPolicy getRenderPolicy() {
if (renderPolicy == null) {
renderPolicy = new StandardRenderPolicy(this);
}
return renderPolicy;
}
/**
* Set the RenderPolicy that determines how the OMGraphicList is rendered.
*/
public void setRenderPolicy(RenderPolicy rp) {
renderPolicy = rp;
// Just to make sure,
rp.setLayer(this);
}
/**
* Sets the SwingWorker off to call prepare(). If the SwingWorker passed in
* is not null, start() is called on it.
*
* @param worker null to reset the layerWorker variable, or a SwingWorker to
* start up.
*/
protected void setLayerWorker(ISwingWorker<OMGraphicList> worker) {
synchronized (LAYERWORKER_LOCK) {
layerWorker = worker;
layerWorkerQueue = false;
}
if (worker != null) {
worker.start();
}
}
protected ISwingWorker<OMGraphicList> getLayerWorker() {
return layerWorker;
}
/**
* Called from within the layer to create a LayerWorker to use for the
* prepare() method. By default, a new LayerWorker is returned. This method
* may be overridden to make the layer use an extended
* LayerWorker/SwingWorker class.
*
* @return SwingWorker/LayerWorker
*/
protected ISwingWorker<OMGraphicList> createLayerWorker() {
return new LayerWorker();
}
/**
* Overrides Layer.dispose(), makes sure the OMGraphicList is cleared.
*/
public void dispose() {
super.dispose();
setList(null);
}
/**
* This method is here to provide a default action for Layers as they act as
* a ProjectionPainter. Normally, ProjectionPainters are expected to receive
* the projection, gather/create OMGraphics that apply to the projection,
* and render them into the Graphics provided. This is supposed to be done
* in the same thread that calls this function, so the caller knows that
* when this method returns, everything that the ProjectionPainter needed to
* do is complete.
* <P>
* If the layer doesn't override this method, then the paint(Graphics)
* method will be called.
*
* @param proj Projection of the map.
* @param g java.awt.Graphics to draw into.
*/
public synchronized void renderDataForProjection(Projection proj, Graphics g) {
if (proj == null) {
logger.warning("Layer(" + getName() + ").renderDataForProjection: null projection!");
return;
} else if (!proj.equals(getProjection())) {
setProjection(proj.makeClone());
setList(getRenderPolicy().prepare());
}
paint(g);
}
/**
* The default action is to get the OMGraphicList and render it.
*
* @param g java.awt.Graphics object to render OMGraphics into.
*/
public void paint(Graphics g) {
getRenderPolicy().paint(g);
}
/**
* A method that will launch a LayerWorker thread to call the prepare
* method. This method will set in motion all the steps needed to create and
* render the current OMGraphicList with the current projection. Nothing
* more needs to be called, because the LayerWorker will be started, it will
* call prepare(). Inside the prepare() method, the OMGraphicList should be
* created and the OMGraphics generated for the current projection that can
* be picked up in the getProjection() method, and the LayerWorker will call
* workerComplete() which will call repaint() on this layer.
*/
public void doPrepare() {
synchronized (LAYERWORKER_LOCK) {
if (layerWorkerQueue) {
return;
}
ISwingWorker<OMGraphicList> currentLayerWorker = layerWorker;
if (currentLayerWorker != null) {
layerWorkerQueue = true;
if (interruptable) {
currentLayerWorker.interrupt();
}
return;
}
setLayerWorker(createLayerWorker());
}
}
/**
* A check to see if the LayerWorker (SwingWorker) exists (is doing
* something).
*/
public boolean isWorking() {
// We don't care if it hasn't been interrupted - since the
// LayerWorker will launch a new thread when things settle out, we
// just want to know if there is a LayerWorker in place. If multiple
// doPrepare() calls come in, we need to ignore all of the requests
// that have come it after the first one that canceled the
// LayerWorker in the first place.
synchronized (LAYERWORKER_LOCK) {
return (layerWorker != null);
}
}
/**
* This is the main method you should be concerned with when overriding this
* class. You have to make sure that this method returns an OMGraphicList
* that is ready to be rendered. That means they need to be generated with
* the current projection, which can be retrieved by calling
* getProjection().
* <P>
*
* This method will be called in a separate thread if doPrepare() is called
* on the layer. This will automatically cause repaint() to be called, which
* lets java know to call paint() on this class.
* <P>
*
* Note that the default action of this method is to get the OMGraphicList
* as it is currently set in the layer, re-projects the list with the
* current projection (calls generate() on them), and then returns the
* current list.
* <P>
*
* If your layer needs to change what is on the list based on what the
* current projection is, you can either clear() the list yourself and add
* new OMGraphics to it (remember to call generate(Projection) on them), and
* return the list. You also have the option of setting a ListResetPCPolicy,
* which will automatically set the list to null when the projection changes
* before calling this method. The OMGraphicHandlerList will ignore a null
* OMGraphicList.
* <P>
*
* NOTE: If you call prepare directly, you may need to call repaint(), too.
* With all invocations of this method that are cause by a projection
* change, repaint() will be called for you.
*
* The method is synchronized in case renderDataForProjection() gets called
* while in the middle of this method. For a different projection, that
* would be bad.
*/
public synchronized OMGraphicList prepare() {
OMGraphicList currentList = getList();
Projection proj = getProjection();
// if the layer hasn't been added to the MapBean
// the projection could be null.
if (currentList != null && proj != null) {
currentList.generate(proj);
}
return currentList;
}
/**
* Lock object used for managing LayerWorker and queue synchronization.
*/
protected final Object LAYERWORKER_LOCK = new Object();
/**
* Check to see if it's likely the current thread will be replaced with
* another one.
*
* @return true if another layer worker is queued up.
*/
public boolean isCancelled() {
synchronized (LAYERWORKER_LOCK) {
return layerWorkerQueue;
}
}
/**
* The LayerWorker calls this method on the layer when it is done working.
* If the calling worker is not the same as the "current" worker, then a new
* worker is created.
*
* @param worker the worker that has the graphics, must not be null.
*/
protected void workerComplete(ISwingWorker<OMGraphicList> worker) {
boolean finishUpWithWorker = false;
synchronized (LAYERWORKER_LOCK) {
finishUpWithWorker = !layerWorkerQueue;
setLayerWorker(layerWorkerQueue ? createLayerWorker() : null);
layerWorkerQueue = false;
}
if (finishUpWithWorker) {
// CAUTION! layer.repaint() is called in workerComplete!!
getProjectionChangePolicy().workerComplete(worker.get());
}
}
/**
* Since we can't have the main thread taking up the time to do the work to
* create OMGraphics, we use this worker thread to do it.
*/
class LayerWorker extends PooledSwingWorker<OMGraphicList> {
/** Constructor used to create a worker thread. */
public LayerWorker() {
super();
}
/**
* Compute the value to be returned by the <code>get</code> method.
*/
public OMGraphicList construct() {
logger.fine(getName() + "|LayerWorker.construct()");
fireStatusUpdate(LayerStatusEvent.START_WORKING);
String errorMsg = null;
try {
long start = System.currentTimeMillis();
OMGraphicList list = getRenderPolicy().prepare();
long stop = System.currentTimeMillis();
if (logger.isLoggable(Level.FINE)) {
logger.fine(getName() + "|LayerWorker.construct(): fetched "
+ (list == null ? "null list " : (list.size() + " graphics ")) + "in "
+ (double) ((stop - start) / 1000d) + " seconds");
}
return list;
} catch (OutOfMemoryError e) {
errorMsg = getName() + "|LayerWorker.construct(): " + e.getMessage();
if (logger.isLoggable(Level.FINER)) {
logger.fine(errorMsg);
e.printStackTrace();
} else {
logger.info(getName() + " layer ran out of memory, attempting to recover...");
}
} catch (Throwable e) {
errorMsg = getName() + "|LayerWorker.construct(): " + e.getClass().getName() + ", "
+ e.getMessage();
logger.info(errorMsg);
e.printStackTrace();
}
// This is only called if there is an error.
if (errorMsg != null && logger.isLoggable(Level.FINE)) {
fireRequestMessage(new InfoDisplayEvent(this, errorMsg));
}
return null;
}
/**
* Called on the event dispatching thread (not on the worker thread)
* after the <code>construct</code> method has returned.
*/
public void finished() {
workerComplete(this);
if (!isInterrupted()) {
fireStatusUpdate(LayerStatusEvent.FINISH_WORKING);
}
}
public String toString() {
return getName() + " LayerWorker";
}
}
/**
* Overrides the Layer setProperties method. Also calls Layer's version. If
* the ProjectionChangePolicy and RenderPolicy objects are set
* programmatically, are PropertyConsumers and the .class property is not
* set, they will still have access to properties if this method is called.
* Their property prefix will be scoped as if the OMGraphicHandlerLayer had
* them created, with their prefix being prefix + . +
* PropertyChangePolicyProperty and prefix + . + RenderPolicyProperty. If
* the .class property is set, then a new policy object will be created and
* replace the one set before this method is called.
*
* @param prefix the token to prefix the property names
* @param props the <code>Properties</code> object
*/
public void setProperties(String prefix, Properties props) {
super.setProperties(prefix, props);
String realPrefix = PropUtils.getScopedPropertyPrefix(prefix);
// Check to see if the layer wants to set its own projection
// change policy.
String pcpString = props.getProperty(realPrefix + ProjectionChangePolicyProperty);
String policyPrefix;
if (pcpString != null) {
policyPrefix = realPrefix + pcpString;
// If the projection change policy is null, try to create
// it.
String pcpClass = props.getProperty(policyPrefix + ".class");
if (pcpClass != null) {
Object obj = ComponentFactory.create(pcpClass, policyPrefix, props);
if (obj != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Layer " + getName() + " setting ProjectionChangePolicy ["
+ obj.getClass().getName() + "]");
}
try {
setProjectionChangePolicy((ProjectionChangePolicy) obj);
} catch (ClassCastException cce) {
logger.warning("Layer "
+ getName()
+ " has "
+ policyPrefix
+ " property defined in properties for ProjectionChangePolicy, but "
+ policyPrefix + ".class property (" + pcpClass
+ ") does not define a valid ProjectionChangePolicy. A "
+ obj.getClass().getName() + " was created instead.");
}
} else {
logger.warning("Layer " + getName() + " has " + policyPrefix
+ " property defined in properties for PropertyChangePolicy, but "
+ policyPrefix
+ ".class property does not define a valid PropertyChangePolicy.");
}
} else if (projectionChangePolicy != null) { // ProjectionChangePolicy
// is not null...
// If the projection change policy is not null and the
// policy is a PropertyConsumer, pass the properties
// to the policy. Note that the property prefix for
// the policy is prefix +
// ProjectionChangePolicyProperty
if (projectionChangePolicy instanceof PropertyConsumer) {
((PropertyConsumer) projectionChangePolicy).setProperties(policyPrefix, props);
}
} else {
logger.warning("Layer " + getName() + " has " + policyPrefix
+ " property defined in properties for PropertyChangePolicy, but "
+ policyPrefix + ".class property is undefined.");
}
} else if (logger.isLoggable(Level.FINE)) {
logger.fine("Layer " + getName() + " using default ProjectionChangePolicy ["
+ getProjectionChangePolicy().getClass().getName() + "]");
}
// Check to see if the layer want to set its own rendering
// policy.
String rpString = props.getProperty(realPrefix + RenderPolicyProperty);
if (rpString != null) {
policyPrefix = realPrefix + rpString;
String rpClass = props.getProperty(policyPrefix + ".class");
if (rpClass != null) {
Object rpObj = ComponentFactory.create(rpClass, policyPrefix, props);
if (rpObj != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Layer " + getName() + " setting RenderPolicy ["
+ rpObj.getClass().getName() + "]");
}
try {
setRenderPolicy((RenderPolicy) rpObj);
} catch (ClassCastException cce) {
logger.warning("Layer " + getName() + " has " + policyPrefix
+ " property defined in properties for RenderPolicy, but "
+ policyPrefix + ".class property (" + rpClass
+ ") does not define a valid RenderPolicy. A "
+ rpObj.getClass().getName() + " was created instead.");
}
} else {
logger.warning("Layer " + getName() + " has " + policyPrefix
+ " property defined in properties for RenderPolicy, but "
+ policyPrefix + ".class property (" + rpClass
+ ") isn't being created.");
}
} else if (renderPolicy != null) { // RenderPolicy is not null...
// Same thing with renderPolicy as with projection
// change policy.
if (renderPolicy instanceof PropertyConsumer) {
((PropertyConsumer) renderPolicy).setProperties(policyPrefix, props);
}
} else {
logger.warning("Layer " + getName() + " has " + policyPrefix
+ " property defined in properties for RenderPolicy, but " + policyPrefix
+ ".class property is undefined.");
}
} else if (logger.isLoggable(Level.FINE)) {
logger.fine("Layer " + getName() + " using default RenderPolicy ["
+ getRenderPolicy().getClass().getName() + "]");
}
String mmString = props.getProperty(realPrefix + MouseModesProperty);
if (mmString != null) {
Vector<String> mmv = PropUtils.parseSpacedMarkers(mmString);
if (!mmv.isEmpty()) {
String[] mm = new String[mmv.size()];
int i = 0;
for (String it : mmv) {
mm[i++] = it;
}
setMouseModeIDsForEvents(mm);
}
}
consumeEvents = PropUtils.booleanFromProperties(props, realPrefix + ConsumeEventsProperty, consumeEvents);
setTransparency(PropUtils.floatFromProperties(props, realPrefix + TransparencyProperty, getTransparency()));
setInterruptable(PropUtils.booleanFromProperties(props, realPrefix + InterruptableProperty, isInterruptable()));
}
/**
* Overrides Layer getProperties method., also calls that method on Layer.
* Sets the properties from the policy objects used by this OMGraphicHandler
* layer.
*/
public Properties getProperties(Properties props) {
props = super.getProperties(props);
String prefix = PropUtils.getScopedPropertyPrefix(this);
String policyPrefix = null;
// //// ProjectionChangePolicy
ProjectionChangePolicy pcp = getProjectionChangePolicy();
if (pcp instanceof PropertyConsumer) {
policyPrefix = ((PropertyConsumer) pcp).getPropertyPrefix();
((PropertyConsumer) pcp).getProperties(props);
}
if (policyPrefix == null) {
policyPrefix = prefix + "pcp";
}
// Whoops, need to make sure pcp is valid but removing the
// OMGHL prefix from the front of the policy prefix (if
// applicable). Same for RenderPolicy
props.put(prefix + ProjectionChangePolicyProperty, policyPrefix.substring(prefix.length()));
// This has to come after the above line, or the above
// property will have a trailing period.
policyPrefix = PropUtils.getScopedPropertyPrefix(policyPrefix);
props.put(policyPrefix + "class", pcp.getClass().getName());
RenderPolicy rp = getRenderPolicy();
if (rp instanceof PropertyConsumer) {
policyPrefix = ((PropertyConsumer) rp).getPropertyPrefix();
((PropertyConsumer) rp).getProperties(props);
}
// /// RenderPolicy
if (policyPrefix == null) {
policyPrefix = prefix + "rp";
}
props.put(prefix + RenderPolicyProperty, policyPrefix.substring(prefix.length()));
// This has to come after the above line, or the above
// property will have a trailing period.
policyPrefix = PropUtils.getScopedPropertyPrefix(policyPrefix);
props.put(policyPrefix + "class", rp.getClass().getName());
props.put(prefix + ConsumeEventsProperty, new Boolean(consumeEvents).toString());
String[] mm = getMouseModeIDsForEvents();
if (mm != null && mm.length > 0) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < mm.length; i++) {
// Don't need any MouseModes that have been scoped to
// the pretty name, those are automatically generated.
if (mm[i].equals(getName())) {
continue;
}
sb.append(mm[i]).append(" ");
}
props.put(prefix + MouseModesProperty, sb.toString());
}
props.put(prefix + TransparencyProperty, Float.toString(getTransparency()));
props.put(prefix + InterruptableProperty, Boolean.toString(isInterruptable()));
return props;
}
/**
* Overrides Layer getProperties method., also calls that method on Layer.
* Sets the properties from the policy objects used by this OMGraphicHandler
* layer.
*/
public Properties getPropertyInfo(Properties list) {
list = super.getPropertyInfo(list);
String policyPrefix = null;
ProjectionChangePolicy pcp = getProjectionChangePolicy();
if (pcp instanceof PropertyConsumer) {
policyPrefix = ((PropertyConsumer) pcp).getPropertyPrefix();
if (policyPrefix != null) {
int index = policyPrefix.indexOf(".");
if (index != -1) {
policyPrefix = policyPrefix.substring(index + 1);
}
((PropertyConsumer) pcp).getPropertyInfo(list);
}
}
if (policyPrefix == null) {
policyPrefix = "pcp";
}
PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, policyPrefix
+ ".class", "Projection Change Policy", "Class name of ProjectionChangePolicy (optional)", null);
RenderPolicy rp = getRenderPolicy();
if (rp instanceof PropertyConsumer) {
policyPrefix = ((PropertyConsumer) rp).getPropertyPrefix();
if (policyPrefix != null) {
int index = policyPrefix.indexOf(".");
if (index != -1) {
policyPrefix = policyPrefix.substring(index + 1);
}
}
((PropertyConsumer) rp).getPropertyInfo(list);
} else {
}
if (policyPrefix == null) {
policyPrefix = "rp";
}
PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, policyPrefix
+ ".class", "Rendering Policy", "Class name of RenderPolicy (optional)", null);
PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, ConsumeEventsProperty, "Consume mouse events", "Flag that tells the layer to consume mouse events, or let other layers use them as well.", "com.bbn.openmap.util.propertyEditor.OnOffPropertyEditor");
PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, MouseModesProperty, "Mouse modes", "Space-separated list of MouseMode IDs to receive events from.", null);
PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, TransparencyProperty, "Transparency", "Transparency setting for layer, between 0 (clear) and 1", null);
PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, InterruptableProperty, "Interruptable", "Flat to set whether the layer should immediately stop performing current work when the projection changes.", "com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");
return list;
}
/**
* The MapMouseInterpreter used to catch the MapMouseEvents and direct them
* to layer as referencing certain OMGraphics. Manages how the layer
* responds to mouse events.
*/
protected MapMouseInterpreter mouseEventInterpreter = null;
/**
* Set the interpreter used to field and interpret MouseEvents, thereby
* calling GestureResponsePolicy methods on this layer.
*/
public synchronized void setMouseEventInterpreter(MapMouseInterpreter mmi) {
if (mmi instanceof StandardMapMouseInterpreter) {
String[] modeList = getMouseModeIDsForEvents();
((StandardMapMouseInterpreter) mmi).setMouseModeServiceList(modeList);
((StandardMapMouseInterpreter) mmi).setConsumeEvents(getConsumeEvents());
}
if (mouseEventInterpreter != null) {
// Remove handle
mouseEventInterpreter.setGRP(null);
}
mmi.setGRP(this);
mouseEventInterpreter = mmi;
}
/**
* Get the interpreter used to field and interpret MouseEvents, thereby
* calling GestureResponsePolicy methods on this layer. This method checks
* to see if any mouse modes ids have been set via the
* getMouseModeIDsForEvents() method, and if there were and the interpreter
* hasn't been set, it will create a StandardMapMouseInterpreter. Otherwise,
* it returns whatever has been set as the interpreter, which could be null.
*/
public synchronized MapMouseInterpreter getMouseEventInterpreter() {
if (getMouseModeIDsForEvents() != null && mouseEventInterpreter == null) {
setMouseEventInterpreter(new StandardMapMouseInterpreter(this));
}
return mouseEventInterpreter;
}
/**
* Query asked from the MouseDelegator for interest in receiving
* MapMouseEvents. This returns a MapMouseInterpreter that has been told to
* listen for events from the MapMouseModes in setMouseModeIDsForEvents().
*/
public MapMouseListener getMapMouseListener() {
MapMouseListener mml = getMouseEventInterpreter();
if (mml != null) {
if (logger.isLoggable(Level.FINE)) {
String[] modes = mml.getMouseModeServiceList();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < modes.length; i++) {
sb.append(modes[i]).append(", ");
}
logger.fine("Layer " + getName() + " returning " + mml.getClass().getName()
+ " as map mouse listener that listens to: " + sb.toString());
}
}
return mml;
}
/**
* A flag to tell the layer to be selfish about consuming MouseEvents it
* receives. If set to true, it will consume events so that other layers
* will not receive the events. If false, lower layers will also receive
* events, which will let them react too. Intended to let other layers
* provide information about what the mouse is over when editing.
*/
public void setConsumeEvents(boolean consume) {
consumeEvents = consume;
if (mouseEventInterpreter instanceof StandardMapMouseInterpreter) {
((StandardMapMouseInterpreter) mouseEventInterpreter).setConsumeEvents(getConsumeEvents());
}
}
public boolean getConsumeEvents() {
return consumeEvents;
}
/**
* This is the important method call that determines what MapMouseModes the
* interpreter for this layer responds to. The MapMouseInterpreter calls
* this so it can respond to MouseDelegator queries. You can
* programmatically call setMouseModeIDsForEvents with the mode IDs to set
* these values, or set the mouseModes property for this layer set to a
* space-separated list of mode IDs.
*/
public String[] getMouseModeIDsForEvents() {
return mouseModeIDs;
}
/**
* Use this method to set which mouse modes this layer responds to. The
* array should contain the mouse mode IDs.
*/
public void setMouseModeIDsForEvents(String[] mm) {
if (logger.isLoggable(Level.FINE)) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < mm.length; i++) {
sb.append(mm[i]).append(" ");
}
logger.fine("For layer " + getName() + ", setting mouse modes to " + sb.toString());
}
mouseModeIDs = mm;
if (mouseEventInterpreter instanceof StandardMapMouseInterpreter) {
((StandardMapMouseInterpreter) mouseEventInterpreter).setMouseModeServiceList(mm);
}
}
/**
* Query asking if OMGraphic is highlight-able, which means that something
* in the GUI should change when the mouse is moved or dragged over the
* given OMGraphic. Highlighting shows that something could happen, or
* provides cursory information about the OMGraphic. Responding true to this
* method may cause getInfoText() and getToolTipTextFor() methods to be
* called (depends on the MapMouseInterpetor).
*/
public boolean isHighlightable(OMGraphic omg) {
return true;
}
/**
* Query asking if an OMGraphic is select-able, or able to be moved, deleted
* or otherwise modified. Responding true to this method may cause select()
* to be called (depends on the MapMouseInterpertor) so the meaning depends
* on what the layer does in select.
*/
public boolean isSelectable(OMGraphic omg) {
return false;
}
/**
* A current list of select OMGraphics.
*/
protected OMGraphicList selectedList;
/**
* Retrieve the list of currently selected OMGraphics.
*/
public OMGraphicList getSelected() {
return selectedList;
}
// //// Reactions
/**
* Fleeting change of appearance for mouse movements over an OMGraphic.
*/
public void highlight(OMGraphic omg) {
omg.select();
omg.generate(getProjection());
repaint();
}
/**
* Notification to set OMGraphic to normal appearance.
*/
public void unhighlight(OMGraphic omg) {
omg.deselect();
omg.generate(getProjection());
repaint();
}
/**
* Designate a list of OMGraphics as selected.
*/
public void select(OMGraphicList list) {
if (list != null) {
for (OMGraphic omg : list) {
if (selectedList == null) {
selectedList = new OMGraphicList();
}
if (omg instanceof OMGraphicList && !((OMGraphicList) omg).isVague()) {
select((OMGraphicList) omg);
} else {
selectedList.add(omg);
}
}
}
}
/**
* Designate a list of OMGraphics as de-selected.
*/
public void deselect(OMGraphicList list) {
if (list != null) {
for (OMGraphic omg : list) {
if (omg instanceof OMGraphicList && !((OMGraphicList) omg).isVague()) {
deselect((OMGraphicList) omg);
} else if (selectedList != null) {
selectedList.remove(omg);
}
}
}
}
/**
* Remove OMGraphics from the layer.
*/
public OMGraphicList cut(OMGraphicList omgl) {
OMGraphicList list = getList();
if (list != null && omgl != null) {
list.removeAll(omgl);
}
return omgl;
}
/**
* Return a copy of OMGraphics.
*/
public OMGraphicList copy(OMGraphicList omgl) {
return (OMGraphicList) omgl.clone();
}
/**
* Add OMGraphics to the Layer.
*/
public void paste(OMGraphicList omgl) {
OMGraphicList list = getList();
list.addAll(omgl);
}
/**
* If applicable, should return a short, informational string about the
* OMGraphic to be displayed in the InformationDelegator. Return null if
* nothing should be displayed.
*/
public String getInfoText(OMGraphic omg) {
return null;
}
/**
* If applicable, should return a tool tip for the OMGraphic. Return null if
* nothing should be shown.
*/
public String getToolTipTextFor(OMGraphic omg) {
return null;
}
/**
* Return a JMenu with contents applicable to a pop-up menu for a location
* over the map. The pop-up doesn't concern any OMGraphics, and should be
* presented for a click on the map background.
*
* @param mme a MapMouseEvent describing the location over where the menu
* items should apply, in case different options are appropriate for
* different places.
* @return a JMenu for the map. Return null or empty List if no input
* required.
*/
public List<Component> getItemsForMapMenu(MapMouseEvent mme) {
return null;
}
/**
* Return a java.util.List containing input for a JMenu with contents
* applicable to a pop-up menu for a location over an OMGraphic.
*
* @return a List containing options for the given OMGraphic. Return null or
* empty list if there are no options.
*/
public List<Component> getItemsForOMGraphicMenu(OMGraphic omg) {
return null;
}
/**
* A query from the MapMouseInterpreter wondering if the
* GestureResponsePolicy wants events pertaining to mouse movements over the
* map that are not over an OMGraphic. If the GestureResponsePolicy responds
* true, then the mouseOver and leftClick methods will be called on the
* GestureResponsePolicy by the interpreter. There is no rightClick method
* that is called, because a right click will always cause a
* getItemsForMapMenu() method to be called.
*/
public boolean receivesMapEvents() {
return false;
}
/**
* A notification that the mouse cursor has been moved over the map, not
* over any of the OMGraphics on the GestureResponsePolicy. This only gets
* called if the response to receiveMapEvents is true.
*
* @param mme MapMouseEvent describing the location of the mouse.
* @return true of this information is to be considered consumed and should
* not be passed to anybody else.
*/
public boolean mouseOver(MapMouseEvent mme) {
return false;
}
/**
* A notification that the mouse has been clicked with the left mouse button
* on the map, and not on any of the OMGraphics. This only gets called if
* the response to receiveMapEvents is true. Right clicks on the map are
* always reported to the getItemsForMapMenu method.
*
* @param mme MapMouseEvent describing the location of the mouse.
* @return true of this information is to be considered consumed and should
* not be passed to anybody else.
*/
public boolean leftClick(MapMouseEvent mme) {
return false;
}
/**
* Create a JPanel that has a slider to control the layer transparency. An
* action listener that calls layer repaint() when the value changes will be
* added to the slider.
*
* @param label
* the label for the panel around the slider.
* @param orientation
* JSlider.HORIZONTAL/JSlider.VERTICAL
* @param initialValue
* an initial transparency value between 0-1, 0 being clear.
* @return JPanel with controls for transparency setting.
*/
public JPanel getTransparencyAdjustmentPanel(String label, int orientation, float initialValue) {
JPanel opaquePanel = new JPanel();
// opaquePanel.setBorder(BorderFactory.createEtchedBorder());
GridBagLayout gridbag = new GridBagLayout();
GridBagConstraints c = new GridBagConstraints();
opaquePanel.setLayout(gridbag);
c.anchor = GridBagConstraints.NORTHWEST;
JLabel jb = new JLabel(i18n.get(OMGraphicHandlerLayer.class, "layerTransparency", "Layer Transparency"));
gridbag.setConstraints(jb, c);
opaquePanel.add(jb);
JSlider opaqueSlide = new JSlider(orientation, 0/* min */, 255/* max */,
(int) (255f * initialValue)/* inital */);
java.util.Hashtable<Integer, JLabel> dict = new java.util.Hashtable<Integer, JLabel>();
dict.put(new Integer(0), new JLabel(i18n.get(OMGraphicHandlerLayer.class, "clearSliderLabel", "clear")));
dict.put(new Integer(255), new JLabel(i18n.get(OMGraphicHandlerLayer.class, "opqueSliderLabel", "opaque")));
opaqueSlide.setLabelTable(dict);
opaqueSlide.setPaintLabels(true);
opaqueSlide.setMajorTickSpacing(50);
opaqueSlide.setPaintTicks(true);
opaqueSlide.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ce) {
JSlider slider = (JSlider) ce.getSource();
if (slider.getValueIsAdjusting()) {
OMGraphicHandlerLayer.this.setTransparency((float) slider.getValue() / 255f);
repaint();
}
}
});
c.gridy = 1;
gridbag.setConstraints(opaqueSlide, c);
opaquePanel.add(opaqueSlide);
return opaquePanel;
}
public JButton getRedrawButton(String tooltip) {
DrawingAttributes da = DrawingAttributes.getDefaultClone();
da.setStroke(new BasicStroke(3));
IconPart reloadSymbol = OpenMapAppPartCollection.getReloadSymbol();
reloadSymbol.setRenderingAttributes(da);
ImageIcon ii = OMIconFactory.getIcon(25, 25, reloadSymbol);
JButton redraw = new JButton(ii);
redraw.setActionCommand(RedrawCmd);
redraw.setToolTipText("Redraw Layer");
redraw.addActionListener(this);
return redraw;
}
public JButton getSettingsButton(String tooltip) {
DrawingAttributes da = DrawingAttributes.getDefaultClone();
da.setStroke(new BasicStroke(3));
IconPart settingsSymbol = OpenMapAppPartCollection.getSettingsSymbol();
settingsSymbol.setRenderingAttributes(da);
ImageIcon ii = OMIconFactory.getIcon(25, 25, settingsSymbol);
JButton redraw = new JButton(ii);
redraw.setActionCommand(DisplayPropertiesCmd);
redraw.setToolTipText(tooltip);
redraw.addActionListener(this);
return redraw;
}
/**
* Get a default settings panel that contains the layer transparency
* setting, the settings button and a layer refresh button.
*
* @param clss
* The class for i18n translations
* @param opaquenessSetting
* The current opaqueness setting, as some fraction of 255. 1 is
* opaque.
* @return JPanel with the components all laid out.
*/
public JPanel getDefaultSettingsPanel(Class<?> clss, float opaquenessSetting) {
JPanel panel = new JPanel();
GridBagLayout gridbag = new GridBagLayout();
GridBagConstraints c = new GridBagConstraints();
panel.setLayout(gridbag);
c.gridx = 0;
c.gridy = 0;
c.gridheight = 2;
c.insets = new Insets(5, 5, 5, 0);
JPanel transPanel = getTransparencyAdjustmentPanel(
i18n.get(clss, "layerTransparencyGUILabel", "Layer Transparency"), JSlider.HORIZONTAL,
opaquenessSetting);
gridbag.setConstraints(transPanel, c);
panel.add(transPanel);
c.gridx = 1;
c.gridheight = 1;
c.insets = new Insets(5, 0, 0, 5);
JButton jb = getSettingsButton(i18n.get(clss, "layerSettingsButtonTooltip", "Change Layer Settings"));
gridbag.setConstraints(jb, c);
panel.add(jb);
c.gridy = 1;
c.insets = new Insets(0, 0, 5, 5);
jb = getRedrawButton(i18n.get(clss, "layerRedrawButtonTooltip", "Refresh Layer"));
gridbag.setConstraints(jb, c);
panel.add(jb);
return panel;
}
/**
* Set the transparency of the layer. This transparency is applied during
* rendering.
*
* @param value
* 0f for clear, 1f for opaque.
*/
public void setTransparency(float value) {
AlphaComposite ac = null;
if (value != 1f) {
ac = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, value);
}
getRenderPolicy().setComposite(ac);
}
/**
* Get the transparency value for this layer.
*
* @return 1 if opaque, 0 for clear.
*/
public float getTransparency() {
float ret = 1f;
RenderPolicy rp = getRenderPolicy();
if (rp != null) {
Composite comp = rp.getComposite();
if (comp instanceof AlphaComposite) {
ret = ((AlphaComposite) comp).getAlpha();
}
}
return ret;
}
/**
* Override of Layer's actionPerformed method, adds the capability that
* calls doPrepare() if the layer is visible and it receives a RedrawCmd
* command. Also calls Layer.actionPerformed(ActionEvent).
*/
public void actionPerformed(ActionEvent e) {
super.actionPerformed(e);
String cmd = e.getActionCommand();
if (cmd == RedrawCmd) {
if (isVisible()) {
doPrepare();
}
}
}
}