//Title: DistanceMouseMode.
//Version: 2.0
//Copyright:
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR DSTO BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.
//
//Author: R. Wathelet
//Company: Theatre Operations Branch, Defence Science & Technology
//Organisation (DSTO)
package com.bbn.openmap.event;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.text.DecimalFormat;
import java.util.Properties;
import java.util.Vector;
import com.bbn.openmap.InformationDelegator;
import com.bbn.openmap.MapBean;
import com.bbn.openmap.MoreMath;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.OMCircle;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMLine;
import com.bbn.openmap.proj.GreatCircle;
import com.bbn.openmap.proj.Length;
import com.bbn.openmap.proj.Planet;
import com.bbn.openmap.proj.ProjMath;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.proj.coords.LatLonPoint;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PropUtils;
/**
* This mouse mode draws a rubberband line and circle between each mouse click
* as the mouse is moved and displays the cumulative distance in nautical miles
* (nm), kilometers (km), statute miles (miles) and the azimuth angle in decimal
* degrees from north on the status bar. Several distance segments are allowed.
* To erase (terminate) double click the mouse.
* <p>
* The mode creates lines and circles, and then calls map.repaint(). As a
* PaintListener to the MapBean, those lines and circles get painted whenever
* the map is painted.
* <p>
* To use this mouse mode in the OpenMap demo (in setWidgets): create the mouse
* mode, such as
* <p>
* DistanceMouseMode distMode = new DistanceMouseMode(true, id,
* DistanceMouseMode.DISTANCE_ALL);
* <p>
* Add the distance mouse mode to the mouse delegator md.addMouseMode(distMode);
* <p>
* This class can easily be extended, for example to create waypoints for
* objects.
* <p>
* NOTE: If some lines are not properly erased (because the mouse went outside
* the map for example), just use the redraw from the menu.
* <P>
*
* You can set the units used for measurements by setting the property:
*
* <pre>
*
* prefix.units= < name for Length.java (km, miles, meters, nm, all) >
*
* </pre>
*
* Note that "all" will display nm, km, and miles.
*
*/
public class DistanceMouseMode extends CoordMouseMode implements PaintListener {
/**
* Mouse mode identifier, is "Distance". This is returned on getID()
*/
public final static transient String modeID = "Distance";
public final static String UnitProperty = "units";
public final static String ShowCircleProperty = "showCircle";
public final static String ShowAngleProperty = "showAngle";
public final static String RepaintToCleanProperty = "repaintToClean";
public transient DecimalFormat df = new DecimalFormat("0.###");
/**
* Special units value for displaying all units ... use only in properties
* file
*/
public final static String AllUnitsPropertyValue = "all";
/**
* rPoint1 is the anchor point of a line segment
*/
protected Point2D rPoint1;
/**
* rPoint2 is the new (current) point of a line segment
*/
protected Point2D rPoint2;
/**
* Flag, true if the mouse has already been pressed
*/
protected boolean drawDistanceObjects = false;
/**
* Vector to store all distance segments, first point and last point pairs
*/
protected Vector<Point2D> segments = new Vector<Point2D>();
/**
* Distance of the current segment
*/
protected double distance = 0;
/**
* The cumulative distance from the first mouse click
*/
protected double totalDistance = 0;
/**
* The line type to be displayed, see OMGraphic. LINETYPE_GREATCIRCLE,
* LINETYPE_RHUMB, LINETYPE_STRAIGHT default LINETYPE_GREATCIRCLE
*/
int lineType = OMGraphic.LINETYPE_GREATCIRCLE;
/**
* To display the rubberband circle, default true
*/
boolean displayCircle = true;
// The unit type, default mile
Length unit = Length.MILE;
// Flag to display the azimuth angle. Default true
boolean showAngle = true;
/**
* Rendering attributes for lines and circles.
*/
protected DrawingAttributes renderAttributes = DrawingAttributes.getDefaultClone();
// The map bean
transient MapBean theMap;
/**
* Construct a DistanceMouseMode. Default constructor. Sets the ID to the
* modeID, and the consume mode to true. You need to setInfoDelegator,
* setUnit and setLineType if you use this constructor.
*/
public DistanceMouseMode() {
this(true);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
renderAttributes.setLinePaint(Color.GRAY);
renderAttributes.setMattingPaint(Color.LIGHT_GRAY);
renderAttributes.setMatted(true);
}
/**
* Construct a DistanceMouseMode. Lets you set the consume mode. If the
* events are consumed, then a MouseEvent is sent only to the first
* MapMouseListener that successfully processes the event. If they are not
* consumed, then all of the listeners get a chance to act on the event. You
* need to setInfoDelegator, setUnit and setLineType if you use this
* constructor.
*
* @param consumeEvents the mode setting.
*/
public DistanceMouseMode(boolean consumeEvents) {
super(modeID, consumeEvents);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
}
/**
* Construct an DistanceMouseMode. For convenience for derived classes.
*
* @param name the ID of the mode.
* @param consumeEvents if true, events are propagated to the first
* MapMouseListener that successfully processes the event, if false,
* events are propagated to all MapMouseListeners. You need to
* setInfoDelegator, setUnit and setLineType if you use this
* constructor.
*/
public DistanceMouseMode(String name, boolean consumeEvents) {
super(name, consumeEvents);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
}
/**
* Construct a DistanceMouseMode. Lets you set the consume mode. If the
* events are consumed, then a MouseEvent is sent only to the first
* MapMouseListener that successfully processes the event. If they are not
* consumed, then all of the listeners get a chance to act on the event. You
* need to the setLineType if you use this constructor.
*
* @param consumeEvents the mode setting.
* @param id the calling object's info delegator.
* @param units the unit of distance that will be displayed, such as
* Length.NM, Length.KM or Length.MILE. If null, display all of them.
*/
public DistanceMouseMode(boolean consumeEvents, InformationDelegator id, Length units) {
super(modeID, consumeEvents);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
infoDelegator = id;
unit = units;
}
/**
* Construct a DistanceMouseMode. Lets you set the consume mode. If the
* events are consumed, then a MouseEvent is sent only to the first
* MapMouseListener that successfully processes the event. If they are not
* consumed, then all of the listeners get a chance to act on the event. You
* need to the setLineType if you use this constructor.
*
* @param consumeEvents the mode setting.
* @param id the calling object's info delegator.
* @param units the unit of distance that will be displayed, such as
* Length.NM, Length.KM or Length.MILE. If null, display all of them.
* @param lType the line type that will be displayed such as
* LINETYPE_GREATCIRCLE, LINETYPE_RHUMB, LINETYPE_STRAIGHT
*/
public DistanceMouseMode(boolean consumeEvents, InformationDelegator id, Length units, int lType) {
super(modeID, consumeEvents);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
infoDelegator = id;
unit = units;
lineType = lType;
}
/**
* Construct a DistanceMouseMode. Lets you set the consume mode. If the
* events are consumed, then a MouseEvent is sent only to the first
* MapMouseListener that successfully processes the event. If they are not
* consumed, then all of the listeners get a chance to act on the event.
*
* @param consumeEvents the mode setting.
* @param id the calling object's info delegator.
*/
public DistanceMouseMode(boolean consumeEvents, InformationDelegator id) {
super(modeID, consumeEvents);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
infoDelegator = id;
}
/**
* Construct a DistanceMouseMode. For convenience for derived classes. Lets
* you set the consume mode. If the events are consumed, then a MouseEvent
* is sent only to the first MapMouseListener that successfully processes
* the event. If they are not consumed, then all of the listeners get a
* chance to act on the event.
*
* @param name the ID of the mode.
* @param consumeEvents the mode setting.
* @param id the calling object's info delegator.
*/
public DistanceMouseMode(String name, boolean consumeEvents, InformationDelegator id) {
super(name, consumeEvents);
// if you really want to change the cursor shape
// setModeCursor(cursor.getPredefinedCursor(cursor.CROSSHAIR_CURSOR));
infoDelegator = id;
}
/**
* Process a mouseClicked event. Erase all drawn lines and circles upon a
* double mouse click
*
* @param e mouse event.
*/
public void mouseClicked(MouseEvent e) {
mouseSupport.fireMapMouseClicked(e);
if (e.getSource() instanceof MapBean) {
// if double (or more) mouse clicked
if (e.getClickCount() >= 2) {
// end of distance path
drawDistanceObjects = false;
cleanUp();
((MapBean) e.getSource()).repaint();
}
}
}
/**
* Process a mouse pressed event. Add the mouse location to the segment
* vector. Calculate the cumulative total distance.
*
* @param e mouse event.
*/
public void mousePressed(MouseEvent e) {
mouseSupport.fireMapMousePressed(e);
e.getComponent().requestFocus();
if (e.getSource() instanceof MapBean) {
// mouse has now been pressed
drawDistanceObjects = true;
// erase the old circle if any
// eraseCircle();
if (theMap == null) {
theMap = (MapBean) e.getSource();
theMap.addPaintListener(this);
}
// anchor the new first point of the line
rPoint1 = theMap.getCoordinates(e);
// ensure the second point is not yet set.
rPoint2 = null;
// add the anchor point to the list of line segments
segments.addElement(rPoint1);
// add the distance to the total distance
totalDistance += distance;
theMap.repaint();
}
}
/**
* Get the line and circle ready for the map repaint based on where the
* mouse is, distance and azimuth angle as the mouse moves. Display distance
* and azimuth angle in on the infoDelegator.
*
* @param e mouse event.
*/
public void mouseMoved(MouseEvent e) {
mouseSupport.fireMapMouseMoved(e);
if (e.getSource() instanceof MapBean) {
// only when the mouse has already been pressed
if (drawDistanceObjects && theMap != null) {
double lat1, lat2, long1, long2;
// set the map bean
// theMap = (MapBean) (e.getSource());
// erase the old line and circle first
// paintRubberband(rPoint1, rPoint2);
// get the current mouse location in latlon
rPoint2 = theMap.getCoordinates(e);
// paint the new line and circle up to the current
// mouse location
// paintRubberband(rPoint1, rPoint2);
theMap.repaint();
if (infoDelegator != null) {
Debug.message("mousemodedetail", "DistanceMouseMode: firing mouse location");
// lat, lon of anchor point
lat1 = rPoint1.getY();
long1 = rPoint1.getX();
// lat, lon of current mouse position
lat2 = rPoint2.getY();
long2 = rPoint2.getX();
distance = GreatCircle.sphericalDistance(ProjMath.degToRad(lat1), ProjMath.degToRad(long1), ProjMath.degToRad(lat2), ProjMath.degToRad(long2));
// calculate azimuth angle dec deg
double azimuth = getSphericalAzimuth(lat1, long1, lat2, long2);
double tmpDistance = totalDistance + distance;
String infoLine = createDistanceInformationLine(rPoint2, tmpDistance, azimuth);
// setup the info event
InfoDisplayEvent info = new InfoDisplayEvent(this, infoLine, InformationDelegator.COORDINATE_INFO_LINE);
// ask the infoDelegator to display the info
infoDelegator.requestInfoLine(info);
}
} else {
fireMouseLocation(e);
}
}
}
/**
* Create the contents of the information line, based on user inputs.
*
* @param llp current lat/lon of the mouse
* @param distance current distance in radians
* @param azimuth direction of last line
* @return String to put in information line.
*/
protected String createDistanceInformationLine(Point2D llp, double distance, double azimuth) {
// setup the distance info to be displayed
String unitInfo = null;
// what unit is asked for
if (unit == null) {
unitInfo = df.format(Length.NM.fromRadians((float) distance)) + Length.NM.getAbbr()
+ ", " + df.format(Length.KM.fromRadians((float) distance))
+ Length.KM.getAbbr() + ", "
+ df.format(Length.MILE.fromRadians((float) distance)) + Length.MILE.getAbbr()
+ " ";
} else {
unitInfo = unit.fromRadians((float) distance) + " " + unit.getAbbr();
}
// add the mouse lat, lon
StringBuffer infoLine = new StringBuffer();
infoLine.append("Lat, Lon (").append(df.format(llp.getY())).append(", ").append(df.format(llp.getX())).append("), distance (");
// add the units
infoLine.append(unitInfo).append(")");
// add the azimuth angle if need be
if (showAngle) {
infoLine.append(", angle (").append(df.format(azimuth)).append(")");
}
return infoLine.toString();
}
/**
* Process a mouseExited event. If a line is being drawn (and mouse go off
* the map), it will be erased. The anchor point rPoint1 is kept in case the
* mouse comes back on the screen. Then, a new line will be drawn with the
* original mouse press position.
*
* @param e mouse event.
*/
public void mouseExited(MouseEvent e) {
mouseSupport.fireMapMouseExited(e);
if (e.getSource() instanceof MapBean) {
// Stop the last moving segment and circle from being rendered.
rPoint2 = null;
}
}
public void setActive(boolean active) {
if (!active) {
cleanUp();
}
}
/**
* Called by the MapBean when it repaints, to let the MouseMode know when to
* update itself on the map. PaintListener interface.
*/
public void listenerPaint(Object source, java.awt.Graphics g) {
if (drawDistanceObjects) {
for (int i = 0; i < segments.size() - 1; i++) {
paintLine((LatLonPoint) (segments.elementAt(i)), (LatLonPoint) (segments.elementAt(i + 1)), g);
}
if (rPoint1 != null && rPoint2 != null) {
paintRubberband(rPoint1, rPoint2, g);
}
}
}
/**
* Draw a rubberband line between two points into the Graphics object.
*
* @param pt1 the anchor point.
* @param pt2 the current (mouse) position.
* @param graphics a java.awt.Graphics object to render into.
*/
public void paintLine(Point2D pt1, Point2D pt2, Graphics graphics) {
Graphics2D g = (Graphics2D) graphics;
if (pt1 != null && pt2 != null && theMap != null) {
// the line connecting the segments
OMLine cLine = new OMLine(pt1.getY(), pt1.getX(), pt2.getY(), pt2.getX(), lineType);
renderAttributes.setTo(cLine);
// get the map projection
Projection proj = theMap.getRotatedProjection();
// prepare the line for rendering
cLine.generate(proj);
// render the line graphic
cLine.render(g);
}
}
/**
* Draw a rubberband circle between two points
*
* @param pt1 the anchor point.
* @param pt2 the current (mouse) position.
* @param graphics a java.awt.Graphics object to render into.
*/
public void paintCircle(Point2D pt1, Point2D pt2, Graphics graphics) {
// do all this only if want to display the rubberband circle
if (displayCircle && theMap != null) {
Graphics2D g = (Graphics2D) graphics;
if (pt1 != null && pt2 != null) {
// first convert degrees to radians
double radphi1 = ProjMath.degToRad(pt1.getY());
double radlambda0 = ProjMath.degToRad(pt1.getX());
double radphi = ProjMath.degToRad(pt2.getY());
double radlambda = ProjMath.degToRad(pt2.getX());
// calculate the circle radius
double dRad = GreatCircle.sphericalDistance(radphi1, radlambda0, radphi, radlambda);
// convert into decimal degrees
double rad = ProjMath.radToDeg(dRad);
// make the circle
OMCircle circle = new OMCircle(pt1.getY(), pt1.getX(), rad);
renderAttributes.setTo(circle);
// get the map projection
Projection proj = theMap.getRotatedProjection();
// prepare the circle for rendering
circle.generate(proj);
// render the circle graphic
circle.render(g);
}
} // end if(displayCircle)
}
/**
* Draw a rubberband line and circle between two points
*
* @param pt1 the anchor point.
* @param pt2 the current (mouse) position.
* @param g a java.awt.Graphics object to render into.
*/
public void paintRubberband(Point2D pt1, Point2D pt2, Graphics g) {
paintLine(pt1, pt2, g);
paintCircle(pt1, pt2, g);
}
/**
* Reset the segments and distances
*/
public void cleanUp() {
// a quick way to clean the vector
segments = new Vector<Point2D>();
// reset the total distance
totalDistance = 0.0;
distance = 0.0;
if (theMap != null) {
theMap.removePaintListener(this);
theMap.repaint();
theMap = null;
}
}
/**
* Return the distance in the chosen unit between two points (in decimal
* degrees). Based on spherical arc distance between two points. See class
* GreatCircle.java
*
* @param phi1 latitude in decimal degrees of start point
* @param lambda0 longitude in decimal degrees of start point
* @param phi latitude in decimal degrees of end point
* @param lambda longitude in decimal degrees of end point
* @param units the unit of distance, DISTANCE_NM, DISTANCE_KM,
* DISTANCE_MILE or all 3 types DISTANCE_ALL
* @return double distance in chosen unit
*/
public double getGreatCircleDist(double phi1, double lambda0, double phi, double lambda,
int units) {
double dist = 0;
// convert arguments to radians
double radphi1 = ProjMath.degToRad(phi1);
double radlambda0 = ProjMath.degToRad(lambda0);
double radphi = ProjMath.degToRad(phi);
double radlambda = ProjMath.degToRad(lambda);
// get the spherical distance in radians between the two
// points
double distRad = (double) GreatCircle.sphericalDistance(radphi1, radlambda0, radphi, radlambda);
// in the chosen unit
if (units == 0)
dist = distRad * Planet.wgs84_earthEquatorialCircumferenceNMiles / MoreMath.TWO_PI;
if (units == 1)
dist = distRad * Planet.wgs84_earthEquatorialCircumferenceKM / MoreMath.TWO_PI;
if (units == 2)
dist = distRad * Planet.wgs84_earthEquatorialCircumferenceMiles / MoreMath.TWO_PI;
return dist;
}
/**
* Return the azimuth angle in decimal degrees from north. Based on
* spherical_azimuth. See class GreatCircle.java
*
* @param phi1 latitude in decimal degrees of start point
* @param lambda0 longitude in decimal degrees of start point
* @param phi latitude in decimal degrees of end point
* @param lambda longitude in decimal degrees of end point
* @return float azimuth angle in degrees
*/
public double getSphericalAzimuth(double phi1, double lambda0, double phi, double lambda) {
// convert arguments to radians
double radphi1 = ProjMath.degToRad(phi1);
double radlambda0 = ProjMath.degToRad(lambda0);
double radphi = ProjMath.degToRad(phi);
double radlambda = ProjMath.degToRad(lambda);
// get the spherical azimuth in radians between the two points
double az = GreatCircle.sphericalAzimuth(radphi1, radlambda0, radphi, radlambda);
return ProjMath.radToDeg(az);
}
/**
* Set the map bean.
*
* @param aMap a map bean
*/
protected void setMapBean(MapBean aMap) {
theMap = aMap;
}
/**
* Return the map bean.
*/
protected MapBean getMapBean() {
return theMap;
}
/**
* Set the unit of distance to be displayed: Length.NM, Length.KM or
* Length.MILE. If null, displays all of them.
*/
public void setUnit(Length units) {
unit = units;
}
/**
* Return the unit of distance being displayed: Length.NM, Length.KM or
* Length.MILE. If null, displays all of them.
*/
public Length getUnit() {
return unit;
}
/**
* Switch the display of the azimuth angle on or off.
*
* @param onOff true to display the azimuth angle, false to turn off
*/
public void showAzimuth(boolean onOff) {
showAngle = onOff;
}
/**
* Whether the display of the azimuth angle on or off.
*/
public boolean getShowAzimuth() {
return showAngle;
}
/**
* Set the line type to be drawn see also OMGraphic
*
* @param lype either LINETYPE_GREATCIRCLE, LINETYPE_RHUMB,
* LINETYPE_STRAIGHT
*/
public void setLineType(int lype) {
lineType = lype;
}
/**
* Return the line type either LINETYPE_GREATCIRCLE, LINETYPE_RHUMB,
* LINETYPE_STRAIGHT
*/
public int getLineType() {
return lineType;
}
/**
* Set the drawing of the rubberband circle on/off.
*
* @param onOff true or false
*/
public void showCircle(boolean onOff) {
displayCircle = onOff;
}
/**
* Get whether the drawing of the rubberband circle on/off.
*/
public boolean getShowCircle() {
return displayCircle;
}
public boolean isDisplayCircle() {
return displayCircle;
}
public void setDisplayCircle(boolean displayCircle) {
this.displayCircle = displayCircle;
}
public boolean isShowAngle() {
return showAngle;
}
public void setShowAngle(boolean showAngle) {
this.showAngle = showAngle;
}
public double getTotalDistance() {
return totalDistance;
}
public void setTotalDistance(double totalDistance) {
this.totalDistance = totalDistance;
}
/**
* PropertyConsumer interface method.
*/
public void setProperties(String prefix, Properties setList) {
super.setProperties(prefix, setList);
renderAttributes.setProperties(prefix, setList);
prefix = PropUtils.getScopedPropertyPrefix(prefix);
String name = setList.getProperty(prefix + UnitProperty);
if (name != null) {
Length length = Length.get(name);
if (length != null) {
setUnit(length);
} else if (name.equals(AllUnitsPropertyValue)) {
setUnit(null);
}
}
showCircle(PropUtils.booleanFromProperties(setList, prefix + ShowCircleProperty, true));
showAzimuth(PropUtils.booleanFromProperties(setList, prefix + ShowAngleProperty, true));
}
/**
* PropertyConsumer interface method.
*/
public Properties getProperties(Properties getList) {
if (getList == null) {
getList = new Properties();
}
String prefix = PropUtils.getScopedPropertyPrefix(this);
String unitValue = (unit != null ? unit.toString() : AllUnitsPropertyValue);
getList.put(prefix + UnitProperty, unitValue);
getList.put(prefix + ShowCircleProperty, new Boolean(getShowCircle()).toString());
getList.put(prefix + ShowAngleProperty, new Boolean(getShowAzimuth()).toString());
renderAttributes.getProperties(getList);
return getList;
}
/**
* PropertyConsumer interface method.
*/
public Properties getPropertyInfo(Properties list) {
list = super.getPropertyInfo(list);
list.put(UnitProperty, "Units to use for measurements, from Length.name possibilities.");
list.put(ShowCircleProperty, "Flag to set whether the range circle is drawn at the end of the line (true/false).");
list.put(ShowAngleProperty, "Flag to note the azimuth angle of the line in the information line (true/false).");
renderAttributes.getPropertyInfo(list);
list.put(initPropertiesProperty, UnitProperty + " " + ShowCircleProperty + " "
+ ShowAngleProperty + " " + DrawingAttributes.linePaintProperty + " "
+ DrawingAttributes.mattingPaintProperty + " " + DrawingAttributes.mattedProperty);
return list;
}
}