// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
package com.bbn.openmap.gui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ButtonGroup;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import com.bbn.openmap.MapBean;
import com.bbn.openmap.MoreMath;
import com.bbn.openmap.event.ProjectionEvent;
import com.bbn.openmap.event.ProjectionListener;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.OMColor;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMLine;
import com.bbn.openmap.omGraphics.OMText;
import com.bbn.openmap.proj.GreatCircle;
import com.bbn.openmap.proj.Length;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.proj.coords.LatLonPoint;
import com.bbn.openmap.util.PropUtils;
/**
* A panel that listens for projection changes and draws a little legend line on
* itself with a distance. Can be modified with Properties.
*
* <pre>
*
* unitOfMeasure=km (or any value from the Length class)
* locationXoffset=-10 (value off the edge of the component to have legend)
* locationYoffset=-10
* width=pixel width of component
* height=pixel height of component
* </pre>
*/
public class EmbeddedScaleDisplayPanel extends OMComponentPanel implements ProjectionListener {
public EmbeddedScaleDisplayPanel() {
super();
setOpaque(false);
setBackground(OMColor.clear);
setPreferredSize(new Dimension(350, 100));
dAttributes.setMattingPaint(new Color(0xffcccccc));
dAttributes.setMatted(true);
}
public EmbeddedScaleDisplayPanel(DrawingAttributes dAtts, Length units) {
this();
setUnitOfMeasure(units.getAbbr());
dAttributes = dAtts;
}
// Default colors to use, if not specified in the properties.
protected String defaultLineColorString = "FFFFFF";
protected String defaultTextColorString = "FFFFFF";
protected String defaultUnitOfMeasureString = "km";
protected int defaultLocationXoffset = -10;
protected int defaultLocationYoffset = -10;
protected int defaultWidth = 150;
protected int defaultHeight = 10;
// property text values
public static final String UnitOfMeasureProperty = "unitOfMeasure";
public static final String LocationXOffsetProperty = "locationXoffset";
public static final String LocationYOffsetProperty = "locationYoffset";
public static final String WidthProperty = "width";
public static final String HeightProperty = "height";
protected Length uom = Length.get(defaultUnitOfMeasureString);
protected int locationXoffset = defaultLocationXoffset;
protected int locationYoffset = defaultLocationYoffset;
protected int width = defaultWidth;
protected int height = defaultHeight;
protected DrawingAttributes dAttributes = DrawingAttributes.getDefaultClone();
private static final long serialVersionUID = 1L;
static final float RADIANS_270 = Length.DECIMAL_DEGREE.toRadians(270);
protected OMGraphicList legend;
public static Logger logger = Logger.getLogger("com.bbn.openmap.gui.EmbedddedScaleDisplayPanel");
/**
* Sets the properties for the <code>Layer</code>. This allows
* <code>Layer</code> s to get a richer set of parameters than the
* <code>setArgs</code> method.
*
* @param prefix the token to prefix the property names
* @param properties the <code>Properties</code> object
*/
public void setProperties(String prefix, Properties properties) {
super.setProperties(prefix, properties);
prefix = com.bbn.openmap.util.PropUtils.getScopedPropertyPrefix(prefix);
dAttributes.setProperties(prefix, properties);
String unitOfMeasure = properties.getProperty(prefix + UnitOfMeasureProperty);
setUnitOfMeasure(unitOfMeasure);
locationXoffset = PropUtils.intFromProperties(properties, prefix + LocationXOffsetProperty, defaultLocationXoffset);
locationYoffset = PropUtils.intFromProperties(properties, prefix + LocationYOffsetProperty, defaultLocationYoffset);
width = PropUtils.intFromProperties(properties, prefix + WidthProperty, defaultWidth);
height = PropUtils.intFromProperties(properties, prefix + HeightProperty, defaultHeight);
}
/**
* Getter for property unitOfMeasure.
*
* @return Value of property unitOfMeasure.
*/
public String getUnitOfMeasure() {
return this.uom.toString();
}
/**
* Setter for property unitOfMeasure.
*
* @param unitOfMeasure New value of property unitOfMeasure.
*/
public void setUnitOfMeasure(String unitOfMeasure) {
if (unitOfMeasure == null)
unitOfMeasure = Length.KM.toString();
// There is a bug in the Length.get() method that will not
// return
// the correct (or any value) for a requested uom.
// This does not work:
// uom = com.bbn.openmap.proj.Length.get(unitOfMeasure);
// Therefore, The following code correctly obtains the proper
// Length object.
Length[] choices = Length.values();
uom = null;
for (int i = 0; i < choices.length; i++) {
if (unitOfMeasure.equalsIgnoreCase(choices[i].toString())
|| unitOfMeasure.equalsIgnoreCase(choices[i].getAbbr())) {
uom = choices[i];
break;
}
}
// of no uom is found assign Kilometers as the default.
if (uom == null)
uom = Length.KM;
}
JPanel palette;
Vector<JRadioButton> buttons = new Vector<JRadioButton>();
ButtonGroup uomButtonGroup;
/** Creates the interface palette. */
public java.awt.Component getGUI() {
if (palette == null) {
logger.fine("creating palette.");
palette = new JPanel();
uomButtonGroup = new ButtonGroup();
palette.setLayout(new javax.swing.BoxLayout(palette, javax.swing.BoxLayout.Y_AXIS));
palette.setBorder(new javax.swing.border.TitledBorder("Unit Of Measure"));
ActionListener al = new ActionListener() {
// We don't have to check for action commands or anything like
// that.
// We know this listener is going to be added to JRadioButtons
// that
// are labeled with abbreviations for length.
public void actionPerformed(ActionEvent e) {
JRadioButton jrb = (JRadioButton) e.getSource();
setUnitOfMeasure(jrb.getText());
}
};
for (Length lengthType : Length.values()) {
JRadioButton jrb = new JRadioButton();
jrb.setText(lengthType.getAbbr());
jrb.setToolTipText(lengthType.toString());
uomButtonGroup.add(jrb);
palette.add(jrb);
jrb.addActionListener(al);
jrb.setSelected(uom.getAbbr().equalsIgnoreCase(lengthType.getAbbr()));
buttons.add(jrb);
}
} else {
for (JRadioButton button : buttons) {
button.setSelected(uom.getAbbr().equalsIgnoreCase(button.getText()));
}
}
return palette;
}
public void projectionChanged(ProjectionEvent e) {
int w, h, left_x = 0, right_x = 0, lower_y = 0, upper_y = 0;
Projection projection = e.getProjection();
OMGraphicList graphics = new OMGraphicList();
w = projection.getWidth();
h = projection.getHeight();
// FIXME: Use the center since it's always real
/**
* Since the pixel space for the component has nothing to do with the
* pixel space of the projection, we'll just use the projection pixel
* space to find out how long the line should be. Then, we'll move that
* length into component pixel space.
*/
lower_y = h / 2;
right_x = w / 2;
left_x = right_x - width;
LatLonPoint loc1 = projection.inverse(left_x, lower_y, new LatLonPoint.Double());
LatLonPoint loc2 = projection.inverse(right_x, lower_y, new LatLonPoint.Double());
double dist = GreatCircle.sphericalDistance(loc1.getRadLat(), loc1.getRadLon(), loc2.getRadLat(), loc2.getRadLon());
// Round the distance to one of the preferred values.
dist = uom.fromRadians(dist);
double new_dist = scopeDistance(dist);
if (logger.isLoggable(Level.FINE)) {
logger.fine("modifying " + dist + " to new distance: " + new_dist);
}
left_x = getPtAtDistanceFromLatLon(loc2, new_dist, projection, uom);
int lineLength = right_x - left_x;
// If the length of the distance line is longer than the width of the
// panel, divide it in half.
int maxWidth = Math.max(getWidth() - Math.abs(locationXoffset) * 2, 50);
while (lineLength > maxWidth) {
lineLength /= 3;
new_dist /= 3.0;
if (logger.isLoggable(Level.FINE)) {
logger.fine("length of line too long, halving to " + lineLength);
}
double testDist = scopeDistance(new_dist);
if (!MoreMath.approximately_equal(testDist, new_dist) && !(new_dist <= .01)) {
lineLength = right_x - getPtAtDistanceFromLatLon(loc2, testDist, projection, uom);
if (logger.isLoggable(Level.FINE)) {
logger.fine("needed to rescope distance to " + testDist + " from " + new_dist);
}
new_dist = testDist;
}
}
// Now, check the units and try to avoid fractions
Length cur_uom = uom;
if (new_dist < 1) {
if (uom.equals(Length.KM)) {
new_dist *= 1000;
cur_uom = Length.METER;
} else if (uom.equals(Length.MILE) || uom.equals(Length.DM) || uom.equals(Length.NM)) {
new_dist = Length.FEET.fromRadians(uom.toRadians(new_dist));
cur_uom = Length.FEET;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("modified UOM to " + cur_uom.getAbbr() + ", value: " + new_dist);
}
double testDist = scopeDistance(new_dist);
if (!MoreMath.approximately_equal(testDist, new_dist)) {
lineLength = right_x
- getPtAtDistanceFromLatLon(loc2, testDist, projection, cur_uom);
if (logger.isLoggable(Level.FINE)) {
logger.fine("needed to rescope distance to " + testDist + " from " + new_dist);
}
new_dist = testDist;
}
}
/**
* Now, figure out where OMGraphics go in the component space.
*/
if (locationXoffset < 0) {
int cw = getWidth();
left_x = cw + locationXoffset - lineLength;
right_x = cw + locationXoffset;
} else if (locationXoffset >= 0) {
left_x = locationXoffset;
right_x = locationXoffset + lineLength;
}
if (locationYoffset < 0) {
int ch = getHeight();
upper_y = ch + locationYoffset - height;
lower_y = ch + locationYoffset;
} else if (locationYoffset >= 0) {
upper_y = locationYoffset;
lower_y = locationYoffset + height;
}
// Draw the lines and the rounded distance string.
OMLine line = new OMLine(left_x, lower_y, right_x, lower_y);
dAttributes.setTo(line);
graphics.add(line);
line = new OMLine(left_x, lower_y, left_x, upper_y);
dAttributes.setTo(line);
graphics.add(line);
line = new OMLine(right_x, lower_y, right_x, upper_y);
dAttributes.setTo(line);
graphics.add(line);
// String outtext;
// if (new_dist < 1.0f) {
// outtext = String.format("%.3f %s", new_dist, cur_uom.getAbbr());
// } else if (new_dist < 10.0f) {
// outtext = String.format("%.2f %s", new_dist, cur_uom.getAbbr());
// } else if (new_dist < 100.0f) {
// outtext = String.format("%.1f %s", new_dist, cur_uom.getAbbr());
// } else {
String outtext = String.format("%.0f %s", new_dist, cur_uom.getAbbr());
// }
OMText text = new OMText(right_x, lower_y - 20, "" + outtext, OMText.JUSTIFY_RIGHT);
Font font = text.getFont();
text.setFont(font.deriveFont(Font.BOLD, font.getSize() + 4));
dAttributes.setTo(text);
text.setTextMatteColor((Color) dAttributes.getMattingPaint());
text.setTextMatteStroke(new BasicStroke(5));
text.setMattingPaint(OMColor.clear);
graphics.add(text);
graphics.generate(projection);
setLegend(graphics);
}
protected int getPtAtDistanceFromLatLon(LatLonPoint loc2, double unitDist,
Projection projection, Length uom) {
double lineWidthInRadians = uom.toRadians(unitDist);
LatLonPoint newX = GreatCircle.sphericalBetween(loc2.getRadLat(), loc2.getRadLon(), lineWidthInRadians, RADIANS_270);
Point2D newLoc1 = projection.forward(newX);
return (int) Math.round(newLoc1.getX());
}
/**
* Take a given distance and round it down to the nearest 1, 2, or 5 (or
* tens/hundreds version of those increments) multiple of that number.
*
* @param dist
* @return scoped value of distance, incremented properly
*/
protected double scopeDistance(double dist) {
double new_dist;
if (dist <= .01) {
new_dist = .01;
} else if (dist <= .02) {
new_dist = .02;
} else if (dist <= .05) {
new_dist = .05;
} else if (dist <= .1) {
new_dist = .1;
} else if (dist <= .2) {
new_dist = .2;
} else if (dist <= .5) {
new_dist = .5;
} else if (dist <= 1) {
new_dist = 1;
} else if (dist <= 2) {
new_dist = 2;
} else if (dist <= 5) {
new_dist = 5;
} else if (dist <= 10) {
new_dist = 10;
} else if (dist <= 20) {
new_dist = 20;
} else if (dist <= 50) {
new_dist = 50;
} else if (dist <= 100) {
new_dist = 100;
} else if (dist <= 200) {
new_dist = 200;
} else if (dist <= 500) {
new_dist = 500;
} else {
new_dist = 1000;
}
return new_dist;
}
public OMGraphicList getLegend() {
return legend;
}
public void setLegend(OMGraphicList legend) {
this.legend = legend;
}
public void paint(Graphics g) {
if (legend != null) {
legend.render(g);
}
}
protected MapBean mapBean;
public void findAndInit(Object someObj) {
super.findAndInit(someObj);
if (someObj instanceof MapBean) {
mapBean = (MapBean) someObj;
mapBean.addProjectionListener(this);
}
}
public void findAndUndo(Object someObj) {
super.findAndUndo(someObj);
if (someObj.equals(mapBean)) {
mapBean.removeProjectionListener(this);
mapBean = null;
}
}
}