// **********************************************************************
//
// <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/shape/ShapeLayer.java,v $
// $RCSfile: ShapeLayer.java,v $
// $Revision: 1.29 $
// $Date: 2008/10/16 17:58:15 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.shape;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import com.bbn.openmap.dataAccess.shape.DbfHandler;
import com.bbn.openmap.io.BinaryBufferedFile;
import com.bbn.openmap.io.BinaryFile;
import com.bbn.openmap.io.FormatException;
import com.bbn.openmap.layer.OMGraphicHandlerLayer;
import com.bbn.openmap.layer.policy.BufferedImageRenderPolicy;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.proj.ProjMath;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.util.DataBounds;
import com.bbn.openmap.util.DataBoundsProvider;
import com.bbn.openmap.util.PropUtils;
/**
* An OpenMap Layer that displays shape files. Note that the ESRIRecords have
* been updated so that the OMGraphics that get created from them are loaded
* with an Integer object that notes the number of the record as it was read
* from the .shp file. This lets you align the object with the correct attribute
* data in the .dbf file.
* <p>
* <pre>
*
* ############################
* # Properties for a shape layer
* shapeLayer.class=com.bbn.openmap.layer.shape.ShapeLayer
* shapeLayer.prettyName=Name_for_Menu
* shapeLayer.shapeFile=<path to shapefile (.shp)>
* shapeLayer.spatialIndex=<path to generated spatial index file (.ssx)>
* shapeLayer.lineColor=ff000000
* shapeLayer.fillColor=ff000000
* # plus any other properties used by the DrawingAttributes object.
* shapeLayer.pointImageURL=<URL for image to use for point objects>
*
*
* # Optionally, Rule marker names specified in space-separated list
* shapeLayer.rules=rule0 rule1
* # global scale settings can be used so work is only performed within scale range of minScale/maxScale
* shapeLayer.maxScale=1000000f
*
* # rule0 definition:
* # CLASS_RTE is a DBF column name
* shapeLayer.rule0.key=CLASS_RTE
* # operation, if key value is less than 2
* shapeLayer.rule0.op=lt
* shapeLayer.rule0.val=2
* # If rule is met, then actions can be performed:
* # DBF Column names can be added together in a label by specifying them in a space-separated list
* shapeLayer.rule0.label=PREFIX PRETYPE NAME TYPE SUFFIX
* # Labels can have scale limits imposed, so they don't appear if map scale is
* # greater than maxScale or less than minScale
* shapeLayer.rule0.label.maxScale=1000000
* # Visibility can be controlled with respect to scale as well
* shapeLayer.rule0.render=true
* shapeLayer.rule0.render.maxScale=1000000
* # Rendering attributes can be specified.
* shapeLayer.rule0.lineColor=FFFA73
* shapeLayer.rule0.lineWidth=4
* shapeLayer.rule0.mattingColor=55AAAAAA
*
* # rule1 definition:
* shapeLayer.rule1.key=CLASS_RTE
* shapeLayer.rule1.op=all
* shapeLayer.rule1.label=PREFIX PRETYPE NAME TYPE SUFFIX
* shapeLayer.rule1.label.maxScale=200000
* shapeLayer.rule1.render=true
* shapeLayer.rule1.render.maxScale=500000
* shapeLayer.rule1.lineColor=FFFFFF
* shapeLayer.rule1.lineWidth=3
* shapeLayer.rule1.mattingColor=55AAAAAA
* ############################
*
* </pre>
*
* @author Tom Mitchell
* @version $Revision: 1.29 $ $Date: 2008/10/16 17:58:15 $
* @see SpatialIndex
*/
public class ShapeLayer extends OMGraphicHandlerLayer implements ActionListener, DataBoundsProvider {
private static final long serialVersionUID = 1L;
public static Logger logger = Logger.getLogger("com.bbn.openmap.layer.shape.ShapeLayer");
/** The name of the property that holds the name of the shape file. */
public final static String shapeFileProperty = "shapeFile";
/** The URL of an image to use for point objects. */
public final static String pointImageURLProperty = "pointImageURL";
/** The spatial index of the shape file to be rendered. */
protected SpatialIndex spatialIndex;
/**
* The DrawingAttributes object to describe the rendering of graphics.
*/
protected DrawingAttributes drawingAttributes = DrawingAttributes.getDefaultClone();
// For writing out to properties file later.
String shapeFileName = null;
String imageURLString = null;
/**
* Initializes an empty shape layer.
*/
public ShapeLayer() {
setProjectionChangePolicy(new com.bbn.openmap.layer.policy.ListResetPCPolicy(this));
setRenderPolicy(new BufferedImageRenderPolicy(this));
}
public ShapeLayer(String shapeFileName) {
this();
spatialIndex = SpatialIndex.locateAndSetShapeData(shapeFileName);
}
public void setSpatialIndex(SpatialIndex si) {
spatialIndex = si;
}
public SpatialIndex getSpatialIndex() {
return spatialIndex;
}
/**
* A call-back method to override in case you want to change the BinaryFile
* used to in the DbfHandler.
*
* @param dbfFileName path to DBF file.
* @return DbfHandler with BinaryFile set in it.
* @throws FormatException
* @throws IOException
*/
protected DbfHandler createDbfHandler(String dbfFileName) throws FormatException, IOException {
BinaryBufferedFile bbf = new BinaryBufferedFile(dbfFileName);
return new DbfHandler(bbf);
}
/**
* This method gets called from setProperties.
*
* @param realPrefix This prefix has already been scoped, which means it is
* an empty string if setProperties was called with a null prefix, or
* it's a String ending with a period if it was defined with
* characters.
* @param props Properties containing information about files and the layer.
*/
protected void setFileProperties(String realPrefix, Properties props) {
shapeFileName = props.getProperty(realPrefix + shapeFileProperty);
shapeFileName = prependDataPathPrefix(shapeFileName);
if (shapeFileName != null && shapeFileName.length() > 0) {
spatialIndex = SpatialIndex.locateAndSetShapeData(shapeFileName);
String dbfFileName = SpatialIndex.dbf(shapeFileName);
try {
if (BinaryFile.exists(dbfFileName)) {
DbfHandler dbfh = createDbfHandler(dbfFileName);
dbfh.setProperties(realPrefix, props);
spatialIndex.setDbf(dbfh);
}
} catch (FormatException fe) {
if (logger.isLoggable(Level.FINE)) {
logger.warning(getName() + ": Couldn't create DBF handler for " + dbfFileName
+ ", FormatException: " + fe.getMessage());
}
} catch (IOException ioe) {
if (logger.isLoggable(Level.FINE)) {
logger.warning(getName() + ": Couldn't create DBF handler for " + dbfFileName
+ ", IOException: " + ioe.getMessage());
}
}
imageURLString = props.getProperty(realPrefix + pointImageURLProperty);
try {
if (imageURLString != null && imageURLString.length() > 0) {
URL imageURL = PropUtils.getResourceOrFileOrURL(this, imageURLString);
ImageIcon imageIcon = new ImageIcon(imageURL);
spatialIndex.setPointIcon(imageIcon);
}
} catch (MalformedURLException murle) {
logger.warning(getName() + ": point image URL not so good: \n\t" + imageURLString);
} catch (NullPointerException npe) {
// May happen if not connected to the internet.
fireRequestMessage("Can't access icon image: \n " + imageURLString);
}
setSpatialIndex(spatialIndex);
} else {
logger.warning(getName() + ": No Shape file was specified:\n\t" + realPrefix
+ shapeFileProperty);
}
}
/**
* Initializes this layer from the given properties.
*
* @param props the <code>Properties</code> holding settings for this layer
*/
public void setProperties(String prefix, Properties props) {
super.setProperties(prefix, props);
drawingAttributes.setProperties(prefix, props);
String realPrefix = PropUtils.getScopedPropertyPrefix(this);
setFileProperties(realPrefix, props);
}
/**
* PropertyConsumer method.
*/
public Properties getProperties(Properties props) {
props = super.getProperties(props);
String prefix = PropUtils.getScopedPropertyPrefix(this);
props.put(prefix + shapeFileProperty, (shapeFileName == null ? "" : shapeFileName));
props.put(prefix + pointImageURLProperty, (imageURLString == null ? "" : imageURLString));
if (drawingAttributes != null) {
drawingAttributes.setPropertyPrefix(getPropertyPrefix());
drawingAttributes.getProperties(props);
} else {
DrawingAttributes da = (DrawingAttributes) DrawingAttributes.DEFAULT.clone();
da.setPropertyPrefix(getPropertyPrefix());
da.getProperties(props);
}
if (spatialIndex != null) {
DbfHandler dbfh = spatialIndex.getDbf();
if (dbfh != null) {
dbfh.getProperties(props);
}
}
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.).
*
* @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);
String dummyMarker = PropUtils.getDummyMarkerForPropertyInfo(getPropertyPrefix(), null);
PropUtils.setI18NPropertyInfo(i18n, list, ShapeLayer.class, dummyMarker, "Rendering Attributes", "Attributes that determine how the shapes will be drawn.", "com.bbn.openmap.omGraphics.DrawingAttributesPropertyEditor");
list.put(initPropertiesProperty, shapeFileProperty + " " + " " + pointImageURLProperty
+ " " + dummyMarker + " "
+ AddToBeanContextProperty + " " + MinScaleProperty + " " + MaxScaleProperty);
PropUtils.setI18NPropertyInfo(i18n, list, ShapeLayer.class, shapeFileProperty, shapeFileProperty, "Location of Shape file - .shp (File, CURL or relative file path).", "com.bbn.openmap.util.propertyEditor.FUPropertyEditor");
PropUtils.setI18NPropertyInfo(i18n, list, ShapeLayer.class, pointImageURLProperty, pointImageURLProperty, "Image file to use for map location of point data (optional).", "com.bbn.openmap.util.propertyEditor.FUPropertyEditor");
return list;
}
public void setDrawingAttributes(DrawingAttributes da) {
drawingAttributes = da;
}
public DrawingAttributes getDrawingAttributes() {
return drawingAttributes;
}
public String getInfoText(OMGraphic omg) {
return (String) omg.getAttribute(OMGraphic.INFOLINE);
}
/**
* If applicable, should return a tool tip for the OMGraphic. Return null if
* nothing should be shown.
*/
public String getToolTipTextFor(OMGraphic omg) {
return (String) omg.getAttribute(OMGraphic.TOOLTIP);
}
/**
* Create the OMGraphics using the shape file and SpatialIndex.
*
* @return OMGraphicList
* @deprecated use prepare() instead.
*/
protected OMGraphicList computeGraphics() {
return prepare();
}
/**
* Create the OMGraphics using the shape file and SpatialIndex.
*
* @return OMGraphicList
*/
public synchronized OMGraphicList prepare() {
Projection projection = getProjection();
boolean DEBUG_FINE = logger.isLoggable(Level.FINE);
if (projection == null) {
if (DEBUG_FINE) {
logger.fine(getName() + ": prepare called with null projection");
}
return new OMGraphicList();
}
if (spatialIndex == null) {
if (DEBUG_FINE) {
logger.fine(getName() + ": spatialIndex is null!");
}
OMGraphicList list = getList();
if (list != null) {
list.generate(projection, true);// all new graphics
return list;
} else {
// What we'd really like to do is make this a buffered layer at
// this point, if we can't find an ssx file and can't create
// one.
return new OMGraphicList();
}
}
Point2D ul = projection.getUpperLeft();
Point2D lr = projection.getLowerRight();
double ulLat = ul.getY();
double ulLon = ul.getX();
double lrLat = lr.getY();
double lrLon = lr.getX();
OMGraphicList list = new OMGraphicList();
// check for date line anomaly on the screen. we check for
// ulLon >= lrLon, but we need to be careful of the check for
// equality because of floating point arguments...
if (ProjMath.isCrossingDateline(ulLon, lrLon, projection.getScale())) {
if (DEBUG_FINE) {
logger.fine("ShapeLayer.computeGraphics(): Dateline is on screen");
}
double ymin = Math.min(ulLat, lrLat);
double ymax = Math.max(ulLat, lrLat);
try {
list = spatialIndex.getOMGraphics(ulLon, ymin, 180.0d, ymax, list, drawingAttributes, projection, coordTransform);
list = spatialIndex.getOMGraphics(-180.0d, ymin, lrLon, ymax, list, drawingAttributes, projection, coordTransform);
} catch (InterruptedIOException iioe) {
// This means that the thread has been interrupted,
// probably due to a projection change. Not a big
// deal, just return, don't do any more work, and let
// the next thread solve all problems.
list = null;
} catch (IOException ex) {
if (DEBUG_FINE) {
ex.printStackTrace();
}
} catch (FormatException fe) {
if (DEBUG_FINE) {
fe.printStackTrace();
}
} catch (NullPointerException npe) {
// Have seen a NPE as a timing issue on startup that resolves
// itself quickly - the shp file is null
if (DEBUG_FINE) {
npe.printStackTrace();
}
}
} else {
double xmin = Math.min(ulLon, lrLon);
double xmax = Math.max(ulLon, lrLon);
double ymin = Math.min(ulLat, lrLat);
double ymax = Math.max(ulLat, lrLat);
try {
list = spatialIndex.getOMGraphics(xmin, ymin, xmax, ymax, list, drawingAttributes, projection, coordTransform);
} catch (InterruptedIOException iioe) {
// This means that the thread has been interrupted,
// probably due to a projection change. Not a big
// deal, just return, don't do any more work, and let
// the next thread solve all problems.
list = null;
} catch (java.io.IOException ex) {
if (DEBUG_FINE) {
ex.printStackTrace();
}
} catch (FormatException fe) {
if (DEBUG_FINE) {
fe.printStackTrace();
}
} catch (NullPointerException npe) {
// Have seen a NPE as a timing issue on startup that resolves
// itself quickly - the shp file is null
if (DEBUG_FINE) {
npe.printStackTrace();
}
}
}
return list;
}
protected transient JPanel box;
public Component getGUI() {
if (box == null) {
box = new JPanel();
box.setLayout(new BoxLayout(box, BoxLayout.Y_AXIS));
box.setAlignmentX(Component.LEFT_ALIGNMENT);
JPanel stuff = new JPanel();
JLabel label = new JLabel(i18n.get(ShapeLayer.class, "rendering_attributes", "Rendering Attributes: "));
stuff.add(label);
// stuff.setLayout(new BoxLayout(stuff,
// BoxLayout.X_AXIS));
// stuff.setAlignmentX(Component.LEFT_ALIGNMENT);
DrawingAttributes da = getDrawingAttributes();
if (da != null) {
stuff.add(da.getGUI());
}
box.add(stuff);
JPanel pal2 = new JPanel();
JButton redraw = new JButton(i18n.get(ShapeLayer.class, "redrawLayerButton", "Redraw Layer"));
redraw.setActionCommand(RedrawCmd);
redraw.addActionListener(this);
pal2.add(redraw);
box.add(pal2);
}
return box;
}
/**
* DataBoundsInformer interface.
*/
public DataBounds getDataBounds() {
DataBounds box = null;
if (spatialIndex != null) {
ESRIBoundingBox bounds = spatialIndex.getBounds();
if (bounds != null) {
box = new DataBounds(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y);
}
}
return box;
}
/**
* Called when the Layer is removed from the MapBean, giving an opportunity
* to clean up.
*/
public void removed(Container cont) {
OMGraphicList list = getList();
if (list != null) {
list.clear();
list = null;
}
if (spatialIndex != null) {
spatialIndex.close(true);
}
}
}