// **********************************************************************
//
// <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/location/LocationLayer.java,v $
// $RCSfile: LocationLayer.java,v $
// $Revision: 1.9 $
// $Date: 2006/01/18 17:44:15 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.location;
/* Java Core */
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.Box;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import com.bbn.openmap.I18n;
import com.bbn.openmap.gui.WindowSupport;
import com.bbn.openmap.layer.DeclutterMatrix;
import com.bbn.openmap.layer.OMGraphicHandlerLayer;
import com.bbn.openmap.layer.policy.BufferedImageRenderPolicy;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PaletteHelper;
import com.bbn.openmap.util.PropUtils;
/**
* The LocationLayer is a layer that displays graphics supplied by
* LocationHandlers. When the layer receives a new projection, it goes to each
* LocationHandler and asks it for additions to the layer's graphic list. The
* LocationHandlers maintain the graphics, and the layer maintains the overall
* list.
*
* The whole idea behind locations is that there are geographic places that are
* to be marked with a graphic, and/or a text label. The location handler
* handles the interface with the source and type of location to be displayed,
* and the LocationLayer deals with all the locations in a generic way. The
* LocationLayer is capable of using more than one LocationHandler.
* <P>
*
* As a side note, a Link is nothing more than a connection between Locations,
* and is an extension of the Location Class. They have a graphic representing
* the link, an optional label, and an extra set of location coordinates.
* <P>
*
* The layer responds to gestures with pop-up menus. Which menu appears depends
* if the gesture affects a graphic.
* <P>
*
* The properties for this layer are:
* <P>
*
* <pre>
*
* ####################################
* # Properties for LocationLayer
* # Use the DeclutterMatrix to declutter the labels.
* locationlayer.useDeclutter=false
* # Which declutter matrix class to use.
* locationlayer.declutterMatrix=com.bbn.openmap.layer.DeclutterMatrix
* # Let the DeclutterMatrix have labels that run off the edge of the map.
* locationlayer.allowPartials=true
* # The list of location handler prefixes - each prefix should then
* # be used to further define the location handler properties.
* locationlayer.locationHandlers=handler1 handler2
* # Then come the handler properties...
* # At the least, each handler should have a .class property
* handler1.class=<handler classname>
* # plus any other properties handler1 needs - check the handler1 documentation.
* ####################################
*
* </pre>
*/
public class LocationLayer extends OMGraphicHandlerLayer {
/** The declutter matrix to use, if desired. */
protected DeclutterMatrix declutterMatrix = null;
/** Flag to use declutter matrix or not. */
protected boolean useDeclutterMatrix = false;
/**
* Flag to let objects appear partially off the edges of the map, when
* decluttering through the declutter matrix.
*/
protected boolean allowPartials = true;
/** Handlers load the data, and manage it for the layer. */
protected final CopyOnWriteArrayList<LocationHandler> dataHandlers = new CopyOnWriteArrayList<LocationHandler>();
// ///////////////////
// Variables to manage the gesturing mechanisms
static final public String recenter = "Re-center map";
static final public String cancel = "Cancel";
public static final String UseDeclutterMatrixProperty = "useDeclutter";
public static final String DeclutterMatrixClassProperty = "declutterMatrix";
public static final String AllowPartialsProperty = "allowPartials";
public static final String LocationHandlerListProperty = "locationHandlers";
/**
* The default constructor for the Layer. All of the attributes are set to
* their default values.
*/
public LocationLayer() {
setRenderPolicy(new BufferedImageRenderPolicy(this));
setMouseModeIDsForEvents(new String[] { "Gestures" });
}
/**
* The properties and prefix are managed and decoded here, for the standard
* uses of the LocationLayer.
*
* @param prefix string prefix used in the properties file for this layer.
* @param properties the properties set in the properties file.
*/
public void setProperties(String prefix, Properties properties) {
super.setProperties(prefix, properties);
String realPrefix = "";
if (prefix != null) {
realPrefix = prefix + ".";
}
setLocationHandlers(realPrefix, properties);
declutterMatrix = (DeclutterMatrix) PropUtils.objectFromProperties(properties, realPrefix
+ DeclutterMatrixClassProperty);
allowPartials = PropUtils.booleanFromProperties(properties, realPrefix
+ AllowPartialsProperty, true);
if (declutterMatrix != null) {
useDeclutterMatrix = PropUtils.booleanFromProperties(properties, realPrefix
+ UseDeclutterMatrixProperty, useDeclutterMatrix);
declutterMatrix.setAllowPartials(allowPartials);
Debug.message("location", "LocationLayer: Found DeclutterMatrix to use");
// declutterMatrix.setXInterval(3);
// declutterMatrix.setYInterval(3);
} else {
useDeclutterMatrix = false;
}
}
public void setDeclutterMatrix(DeclutterMatrix dm) {
declutterMatrix = dm;
}
public DeclutterMatrix getDeclutterMatrix() {
return declutterMatrix;
}
public void setUseDeclutterMatrix(boolean set) {
useDeclutterMatrix = set;
if (declutterButton != null) {
declutterButton.setSelected(useDeclutterMatrix);
}
}
public boolean getUseDeclutterMatrix() {
return useDeclutterMatrix;
}
/**
* Tell the location handlers to reload their data from their sources. If
* you want these changes to appear on the map, you should call doPrepare()
* after this call.
*/
public void reloadData() {
if (dataHandlers != null) {
for (LocationHandler dataHandler : dataHandlers) {
dataHandler.reloadData();
}
}
}
/**
* Prepares the graphics for the layer. This is where the getRectangle()
* method call is made on the location.
* <p>
* Occasionally it is necessary to abort a prepare call. When this happens,
* the map will set the cancel bit in the LayerThread, (the thread that is
* running the prepare). If this Layer needs to do any cleanups during the
* abort, it should do so, but return out of the prepare asap.
*/
public synchronized OMGraphicList prepare() {
OMGraphicList omGraphicList = new OMGraphicList();
omGraphicList.setTraverseMode(OMGraphicList.FIRST_ADDED_ON_TOP);
Projection projection = getProjection();
if (projection == null) {
if (Debug.debugging("location")) {
Debug.output(getName()
+ "|LocationLayer.prepare(): null projection, layer not ready.");
}
return omGraphicList;
}
if (Debug.debugging("location")) {
Debug.output(getName() + "|LocationLayer.prepare(): doing it");
}
if (useDeclutterMatrix && declutterMatrix != null) {
declutterMatrix.setWidth(projection.getWidth());
declutterMatrix.setHeight(projection.getHeight());
declutterMatrix.create();
}
// Setting the OMGraphicsList for this layer. Remember, the
// Vector is made up of OMGraphics, which are generated
// (projected) when the graphics are added to the list. So,
// after this call, the list is ready for painting.
// call getRectangle();
if (Debug.debugging("location")) {
Debug.output(getName() + "|LocationLayer.prepare(): "
+ "calling prepare with projection: " + projection + " ul = "
+ projection.getUpperLeft() + " lr = " + projection.getLowerRight());
}
Point2D ul = projection.getUpperLeft();
Point2D lr = projection.getLowerRight();
if (Debug.debugging("location")) {
double delta = lr.getX() - ul.getX();
Debug.output(getName() + "|LocationLayer.prepare(): " + " ul.lon =" + ul.getX()
+ " lr.lon = " + lr.getY() + " delta = " + delta);
}
if (dataHandlers != null) {
for (LocationHandler dataHandler : dataHandlers) {
dataHandler.get((float) ul.getY(), (float) ul.getX(), (float) lr.getY(), (float) lr.getX(), omGraphicList);
}
}
// ///////////////////
// safe quit
int size = 0;
if (omGraphicList != null) {
size = omGraphicList.size();
if (Debug.debugging("basic")) {
Debug.output(getName() + "|LocationLayer.prepare(): finished with " + size
+ " graphics");
}
// Don't forget to project them. Since they are only
// being recalled if the projection has changed, then
// we need to force a re-projection of all of them
// because the screen position has changed.
for (OMGraphic thingy : omGraphicList) {
if (useDeclutterMatrix && thingy instanceof Location) {
((Location) thingy).generate(projection, declutterMatrix);
} else {
thingy.generate(projection);
}
}
} else if (Debug.debugging("basic")) {
Debug.output(getName() + "|LocationLayer.prepare(): finished with null graphics list");
}
return omGraphicList;
}
/**
* Paints the layer.
*
* @param g the Graphics context for painting
*/
public void paint(java.awt.Graphics g) {
if (Debug.debugging("location")) {
Debug.output(getName() + "|LocationLayer.paint()");
}
OMGraphicList omgList = getList();
if (omgList != null) {
// Draw from the bottom up, so it matches the palette, and
// the order in which the handlers were loaded - the first
// in the list is on top.
// We need to go through list twice. The first time, draw
// all the regular OMGraphics, and also draw all of the
// graphics for the locations. The second time through,
// draw the labels. This way, the labels won't be covered
// up by graphics.
// render locations
for (OMGraphic omg : omgList) {
if (omg instanceof Location) {
((Location) omg).renderLocation(g);
} else {
omg.render(g);
}
}
// Now render labels
for (OMGraphic omg : omgList) {
if (omg instanceof Location) {
((Location) omg).renderName(g);
}
}
} else {
if (Debug.debugging("location")) {
Debug.error(getName() + "|LocationLayer: paint(): Null list...");
}
}
}
/**
* Parse the properties and set up the location handlers. The prefix will
* should be null, or a prefix string with a period at the end, for scoping
* purposes.
*/
protected void setLocationHandlers(String prefix, Properties p) {
String sPrefix = PropUtils.getScopedPropertyPrefix(prefix);
String handlersValue = p.getProperty(sPrefix + LocationHandlerListProperty);
if (Debug.debugging("location")) {
Debug.output(getName() + "| handlers = \"" + handlersValue + "\"");
}
if (handlersValue == null) {
if (Debug.debugging("location")) {
Debug.output("No property \"" + prefix + LocationHandlerListProperty
+ "\" found in application properties.");
}
return;
}
Vector<String> handlerNames = PropUtils.parseSpacedMarkers(handlersValue);
for (String handlerName : handlerNames) {
String classProperty = handlerName + ".class";
String className = p.getProperty(classProperty);
if (className == null) {
Debug.error("Failed to locate property \"" + classProperty
+ "\"\nSkipping handler \"" + handlerName + "\"");
continue;
}
try {
if (Debug.debugging("location")) {
Debug.output("OpenMap.getHandlers():instantiating handler \"" + className
+ "\"");
}
// Works for applet!
Object obj = Class.forName(className).newInstance();
if (obj instanceof LocationHandler) {
LocationHandler lh = (LocationHandler) obj;
lh.setProperties(handlerName, p);
lh.setLayer(this);
dataHandlers.add(lh);
}
if (false) {
throw new java.io.IOException();// fool javac compiler
}
} catch (java.lang.ClassNotFoundException e) {
Debug.error("Handler class not found: \"" + className + "\"\nSkipping handler \""
+ handlerName + "\"");
} catch (java.io.IOException e) {
Debug.error("IO Exception instantiating class \"" + className
+ "\"\nSkipping handler \"" + handlerName + "\"");
} catch (Exception e) {
Debug.error("Exception instantiating class \"" + className + "\": " + e);
}
}
}
/**
* Let the LocationHandlers know that the layer has been removed.
*/
public void removed(java.awt.Container cont) {
if (dataHandlers != null) {
for (LocationHandler dataHandler : dataHandlers) {
dataHandler.removed(cont);
}
}
}
/**
* Set the LocationHandlers for the layer. Make sure you update the
* LocationHandler names, too, so the names correspond to these.
*
* @param handlers an array of LocationHandlers.
*/
public void setLocationHandlers(LocationHandler[] handlers) {
for (LocationHandler handler : dataHandlers) {
handler.removed(null);
}
dataHandlers.clear();
// Need to set the layer on the handlers.
for (LocationHandler dataHandler : handlers) {
dataHandler.setLayer(this);
dataHandlers.add(dataHandler);
}
resetPalette();
}
/**
* Get the LocationHandlers for this layer.
*/
public LocationHandler[] getLocationHandlers() {
return dataHandlers.toArray(new LocationHandler[dataHandlers.size()]);
}
/**
* Called when the LocationHandlers are reset, or their names are reset, to
* refresh the palette with the new information.
*/
protected void resetPalette() {
box = null;
super.resetPalette();
}
/**
* Overridden from Layer because we are placing our own scroll pane around
* the LocationHandler GUIs.
*/
protected WindowSupport createWindowSupport() {
return new WindowSupport(getGUI(), getName());
}
// ----------------------------------------------------------------------
// GUI
// ----------------------------------------------------------------------
protected Box box = null;
protected JCheckBox declutterButton = null;
/**
* Provides the palette widgets to control the options of showing maps, or
* attribute text.
*
* @return Component object representing the palette widgets.
*/
public java.awt.Component getGUI() {
if (box == null) {
box = Box.createVerticalBox();
int nHandlers = 0;
if (dataHandlers != null) {
nHandlers = dataHandlers.size();
}
Box box2 = Box.createVerticalBox();
for (LocationHandler dataHandler : dataHandlers) {
Component guiComponent = dataHandler.getGUI();
if (guiComponent != null) {
JPanel panel = PaletteHelper.createPaletteJPanel(dataHandler.getPrettyName());
panel.add(dataHandler.getGUI());
box2.add(panel);
}
}
JScrollPane scrollPane = new JScrollPane(box2, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);
scrollPane.setAlignmentY(java.awt.Component.TOP_ALIGNMENT);
box.add(scrollPane);
if (declutterMatrix != null) {
JPanel dbp = new JPanel(new GridLayout(0, 1));
declutterButton = new JCheckBox(i18n.get(LocationLayer.class, "declutterNames", "Declutter Names"), useDeclutterMatrix);
declutterButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JCheckBox jcb = (JCheckBox) ae.getSource();
useDeclutterMatrix = jcb.isSelected();
if (isVisible()) {
doPrepare();
}
}
});
declutterButton.setToolTipText(i18n.get(LocationLayer.class, "declutterNames", I18n.TOOLTIP, "<HTML><BODY>Move location names so they don't overlap.<br>This may take awhile if you are zoomed out.</BODY></HTML>"));
dbp.add(declutterButton);
box.add(dbp);
}
}
return box;
}
public String getToolTipTextFor(OMGraphic omg) {
String ttText = null;
if (omg instanceof Location) {
ttText = ((Location) omg).getName();
}
return ttText;
}
public List<Component> getItemsForOMGraphicMenu(OMGraphic omg) {
if (omg instanceof Location) {
Location loc = (Location) omg;
LocationHandler lh = loc.getLocationHandler();
if (lh != null) {
return lh.getItemsForPopupMenu(loc);
}
}
return null;
}
// protected void showLocationPopup(MouseEvent evt, Location loc, MapBean
// map) {
// if (locMenu == null) {
// locMenu = new LocationPopupMenu();
// locMenu.setMap(map);
// }
// locMenu.removeAll();
//
// locMenu.setEvent(evt);
// locMenu.setLoc(loc);
//
// locMenu.add(new LocationMenuItem(LocationLayer.recenter, locMenu, this));
// locMenu.add(new LocationMenuItem(LocationLayer.cancel, locMenu, this));
// locMenu.addSeparator();
//
//
// locMenu.show(this, evt.getX(), evt.getY());
// }
/**
* PropertyConsumer method, to fill in a Properties object, reflecting the
* current values of the layer. If the layer has a propertyPrefix set, the
* property keys should have that prefix plus a separating '.' prepended to
* each property key it uses for configuration.
*
* @param props a Properties object to load the PropertyConsumer properties
* into. If props equals null, then a new Properties object should be
* created.
* @return Properties object containing PropertyConsumer property values. If
* getList was not null, this should equal getList. Otherwise, it
* should be the Properties object created by the PropertyConsumer.
*/
public Properties getProperties(Properties props) {
props = super.getProperties(props);
String prefix = PropUtils.getScopedPropertyPrefix(this);
props.put(prefix + UseDeclutterMatrixProperty, new Boolean(useDeclutterMatrix).toString());
if (declutterMatrix != null) {
props.put(prefix + DeclutterMatrixClassProperty, declutterMatrix.getClass().getName());
props.put(prefix + AllowPartialsProperty, new Boolean(declutterMatrix.isAllowPartials()).toString());
}
StringBuffer handlerList = new StringBuffer();
// Need to hand this off to the location handlers, and build a
// list of marker names to use in the LocationLayer property
// list.
if (dataHandlers != null) {
for (LocationHandler dataHandler : dataHandlers) {
dataHandler.getProperties(props);
}
}
props.put(prefix + LocationHandlerListProperty, handlerList.toString());
return props;
}
/**
* Method to fill in a Properties object with values reflecting the
* properties able to be set on this PropertyConsumer. The key for each
* property should be the raw property name (without a prefix) with a value
* that is a String that describes what the property key represents, along
* with any other information about the property that would be helpful
* (range, default value, etc.). For Layer, this method should at least
* return the 'prettyName' property.
*
* @param list a Properties object to load the PropertyConsumer properties
* into. If getList equals null, then a new Properties object should
* be created.
* @return Properties object containing PropertyConsumer property values. If
* getList was not null, this should equal getList. Otherwise, it
* should be the Properties object created by the PropertyConsumer.
*/
public Properties getPropertyInfo(Properties list) {
list = super.getPropertyInfo(list);
PropUtils.setI18NPropertyInfo(i18n, list, LocationLayer.class, UseDeclutterMatrixProperty, "Use Declutter Matrix", "Flag for using the declutter matrix.", "com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");
PropUtils.setI18NPropertyInfo(i18n, list, LocationLayer.class, DeclutterMatrixClassProperty, "Declutter Matrix Class", "Class name of the declutter matrix to use (com.bbn.openmap.layer.DeclutterMatrix).", null);
PropUtils.setI18NPropertyInfo(i18n, list, LocationLayer.class, AllowPartialsProperty, "Allow partials", "Flag to allow labels to run off the edge of the map.", "com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");
PropUtils.setI18NPropertyInfo(i18n, list, LocationLayer.class, LocationHandlerListProperty, "Location Handlers", "Space-separated list of unique names to use to scope the LocationHandler property definitions.", null);
if (dataHandlers != null) {
for (LocationHandler dataHandler : dataHandlers) {
dataHandler.getPropertyInfo(list);
}
}
return list;
}
}