// **********************************************************************
//
// <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/Location.java,v $
// $RCSfile: Location.java,v $
// $Revision: 1.12 $
// $Date: 2009/01/21 01:24:42 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.location;
/* Java Core */
import java.awt.Graphics;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.bbn.openmap.layer.DeclutterMatrix;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicAdapter;
import com.bbn.openmap.omGraphics.OMPoint;
import com.bbn.openmap.omGraphics.OMText;
import com.bbn.openmap.proj.Projection;
/**
* A Location is a place. It can be thought of as a lat/lon place, with or
* without an pixel offset, or a place on the screen. A location is basically
* thought of as having a name, which get represented as a label, and some
* graphical representation. It is abstract because it doesn't really know what
* kind of markers or labels are being used or how they are being positioned
* around the particular point. Therefore, it should be extended, and the
* setGraphicLocations methods implemented to position the marker and text as
* desired.
* <P>
*/
public abstract class Location
extends OMGraphicAdapter {
protected static Logger logger = Logger.getLogger("com.bbn.openmap.layer.location.Location");
/**
* The main latitude of object, in decimal degrees, for RENDERTYPE_LATLON
* and RENDERTYPE_OFFSET locations.
*/
public double lat = 0.0f;
/**
* The main longitude of object, in decimal degrees, for RENDERTYPE_LATLON
* and RENDERTYPE_OFFSET locations.
*/
public double lon = 0.0f;
/**
* The x pixel offset from the longitude, for RENDERTYPE_OFFSET locations.
*/
public int xOffset = 0;
/**
* The y pixel offset from the latitude, for RENDERTYPE_OFFSET locations.
*/
public int yOffset = 0;
/** The x object location, in pixels, for RENDERTYPE_XY locations. */
public int x = 0;
/** The y object location, in pixels, for RENDERTYPE_XY locations. */
public int y = 0;
/** The name of the location. */
public String name = null;
/**
* The LocationHandler that is handling the location. Need this to check for
* more global settings for rendering.
*/
public LocationHandler handler;
public final static int DECLUTTER_LOCALLY = -1;
public final static int DECLUTTER_ANYWHERE = -2;
/** The Label of the object. */
protected OMText label = null;
/** The simple location marker of the object. */
protected OMGraphic location = null;
/** The URL to display when the object is gestured upon. */
protected String details = "";
/** The flag for displaying the location marker. */
protected boolean showLocation = true;
/** The flag for displaying the name label. */
protected boolean showName = true;
/**
* The original offset/y location, kept for resetting the placement of the
* label after decluttering and/or location placement.
*/
public int origYLabelOffset = 0;
/**
* The original offset/x location, kept for resetting the placement of the
* label after decluttering and/or location placement.
*/
public int origXLabelOffset = 0;
/**
* the default distance away a label should be placed from a location
* marker.
*/
public final static int DEFAULT_SPACING = 6;
/**
* The pixel limit where the declutter matrix won't draw the name, if it
* can't put the name at least this close to the original place.
* DECLUTTER_LOCALLY keeps the limit to twice the height of the label.
* DECLUTTER_ANYWHERE will place the thing anywhere it fits. Anything else
* is the pixel limit.
*/
protected int declutterLimit = DECLUTTER_LOCALLY;
/** Set whether you want this location label decluttered. */
protected boolean allowDecluttering = true;
/**
* The horizontal pixel distance you want to place the text away from the
* actual location - to put space between the graphic.
*/
protected int horizontalLabelBuffer = 0;
/**
* A plain constructor if you are planning on setting everything yourself.
*/
public Location() {
}
/**
* Create a location at a latitude/longitude. If the locationMarker is null,
* a small rectangle (dot) will be created to mark the location.
*
* @param latitude the latitude, in decimal degrees, of the location.
* @param longitude the longitude, in decimal degrees, of the location.
* @param name the name of the location, also used in the label.
* @param locationMarker the OMGraphic to use for the location mark.
*/
public Location(double latitude, double longitude, String name, OMGraphic locationMarker) {
setLocation(latitude, longitude);
this.name = name;
if (logger.isLoggable(Level.FINER)) {
logger.finer("Location Lat/Lon(" + latitude + ", " + longitude + ", " + name + ")");
}
if (locationMarker == null) {
location = new OMPoint(lat, lon);
} else {
location = locationMarker;
}
// We can do the x offset off the location here, we'll do the
// vertical offset later, when we can figure out the height of
// the text and can line the middle of the text up with the
// location.
// If the caller has supplied a substitute graphic for
// the location spot, it's up to them to horizontally
// offset the label appropriately. They should do that
// here, or in an extended class.
label = new OMText(lat, lon, 0, 0, name, OMText.JUSTIFY_LEFT);
}
/**
* Create a location at a map location. If the locationMarker is null, a
* small rectangle (dot) will be created to mark the location.
*
* @param x the pixel location of the object from the let of the map.
* @param y the pixel location of the object from the top of the map
* @param name the name of the location, also used in the label.
* @param locationMarker the OMGraphic to use for the location mark.
*/
public Location(int x, int y, String name, OMGraphic locationMarker) {
setLocation(x, y);
this.name = name;
if (logger.isLoggable(Level.FINER)) {
logger.finer("Location XY(" + x + ", " + y + ", " + name + ")");
}
if (locationMarker == null) {
location = new OMPoint(x, y);
} else {
location = locationMarker;
}
// We can do the x offset off the location here, we'll do the
// vertical offset later, when we can figure out the height of
// the text and can line the middle of the text up with the
// location.
// If the caller has supplied a substitute graphic for
// the location spot, it's up to them to horizontally
// offset the label appropriately. They should do that
// here, or in an extended class.
label = new OMText(x, y, name, OMText.JUSTIFY_LEFT);
}
/**
* Create a location at a pixel offset from a latitude/longitude. If the
* locationMarker is null, a small rectangle (dot) will be created to mark
* the location.
*
* @param latitude the latitude, in decimal degrees, of the location.
* @param longitude the longitude, in decimal degrees, of the location.
* @param xOffset the pixel location of the object from the longitude.
* @param yOffset the pixel location of the object from the latitude.
* @param name the name of the location, also used in the label.
* @param locationMarker the OMGraphic to use for the location mark.
*/
public Location(double latitude, double longitude, int xOffset, int yOffset, String name, OMGraphic locationMarker) {
setLocation(latitude, longitude, xOffset, yOffset);
this.name = name;
if (logger.isLoggable(Level.FINER)) {
logger.finer("Location(" + latitude + ", " + longitude + ", offset " + x + ", " + y + ", " + name + ")");
}
if (locationMarker == null) {
location = new OMPoint(lat, lon, xOffset, yOffset);
} else {
location = locationMarker;
}
// We can do the x offset off the location here, we'll do the
// vertical offset later, when we can figure out the height of
// the text and can line the middle of the text up with the
// location.
// If the caller has supplied a substitute graphic for
// the location spot, it's up to them to horizontally
// offset the label appropriately. They should do that
// here, or in an extended class.
label = new OMText(lat, lon, xOffset, yOffset, name, OMText.JUSTIFY_LEFT);
}
/** Set the placement of the location. */
public void setLocation(double latitude, double longitude) {
lat = latitude;
lon = longitude;
origYLabelOffset = 0;
origXLabelOffset = DEFAULT_SPACING;
setRenderType(RENDERTYPE_LATLON);
if (location != null && label != null) {
setGraphicLocations(latitude, longitude);
}
}
/** Set the placement of the location. */
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
origYLabelOffset = y;
origXLabelOffset = x + DEFAULT_SPACING;
setRenderType(RENDERTYPE_XY);
if (location != null && label != null) {
setGraphicLocations(x, y);
}
}
/** Set the placement of the location. */
public void setLocation(double latitude, double longitude, int xOffset, int yOffset) {
lat = latitude;
lon = longitude;
this.xOffset = xOffset;
this.yOffset = yOffset;
origYLabelOffset = yOffset;
origXLabelOffset = xOffset + DEFAULT_SPACING;
setRenderType(RENDERTYPE_OFFSET);
if (location != null && label != null) {
setGraphicLocations(latitude, longitude, xOffset, yOffset);
}
}
/**
* Convenience method that lets you provide a screen x, y and a projection
* to the location, and let the location hash out how to place itself based
* on it's rendertype.
*/
public void setLocation(int x, int y, Projection proj) {
int renderType = getRenderType();
switch (renderType) {
case RENDERTYPE_LATLON:
if (proj != null) {
Point2D llp = proj.inverse(x, y);
setLocation((float) llp.getY(), (float) llp.getX());
} else {
logger.fine("Location can't set lat/lon coordinates without a projection");
}
break;
case RENDERTYPE_OFFSET:
if (proj != null) {
Point2D llp = proj.inverse(x, y);
setLocation((float) llp.getY(), (float) llp.getX(), this.xOffset, this.yOffset);
} else {
logger.fine("Location can't set lat/lon coordinates without a projection");
}
break;
default:
setLocation(x, y);
}
}
public abstract void setGraphicLocations(double latitude, double longitude);
public abstract void setGraphicLocations(int x, int y);
public abstract void setGraphicLocations(double latitude, double longitude, int offsetX, int offsetY);
/**
* Set the location handler for the location.
*/
public void setLocationHandler(LocationHandler lh) {
handler = lh;
}
/**
* Get the location handler for the location.
*/
public LocationHandler getLocationHandler() {
return handler;
}
/**
* Set the edge java.awt.Paint for the marker graphic.
*/
public void setLocationPaint(Paint locationPaint) {
if (location != null) {
location.setLinePaint(locationPaint);
}
}
/**
* Get the label for the location.
*/
public OMText getLabel() {
return label;
}
/**
* Set the label for the location.
*/
public void setLabel(OMText lable) {
label = lable;
}
/**
* Get the location marker for this location.
*/
public OMGraphic getLocationMarker() {
return location;
}
/**
* Set the graphic for the location.
*/
public void setLocationMarker(OMGraphic graphic) {
location = graphic;
}
/**
* Set whether this location should be shown on an individual basis.
*/
public void setShowLocation(boolean showLocations) {
showLocation = showLocations;
}
/** See of the location is displaying it's location. */
public boolean isShowLocation() {
return showLocation;
}
/** Set the location to display it's label. */
public void setShowName(boolean showNames) {
showName = showNames;
}
/** See if the location is displaying it's label. */
public boolean isShowName() {
return showName;
}
/** Get the name of the location. */
public String getName() {
return name;
}
/**
* Set the name of this location.
*/
public void setName(String name) {
this.name = name;
if (label != null) {
label.setData(name);
}
}
/**
* Set the details for the location. This should be the contents to be
* displayed in a web browser.
*/
public void setDetails(String det) {
details = det;
}
/**
* Get the details for the location.
*/
public String getDetails() {
return details;
}
/**
* Fire a browser to display the location details.
*/
public void showDetails() {
if (details != null) {
try {
getLocationHandler().getLayer().fireRequestBrowserContent(details);
} catch (NullPointerException npe) {
}
}
}
/**
* Set whether you want to allow the label for this location to be
* decluttered.
*
* @param allow if true, label will be decluttered if declutter matrix is
* available.
*/
public void setAllowDecluttering(boolean allow) {
allowDecluttering = allow;
}
/**
* Get the decluttering allowance setting for this label.
*/
public boolean isAllowDecluttering() {
return allowDecluttering;
}
/**
* Set the pixel distance that the label will be moved to the right, to
* clear space for the graphic marking the location.
*/
public void setHorizontalLabelBuffer(int buffer) {
horizontalLabelBuffer = buffer;
}
/**
* Get the pixel distance that the label will be moved to the right, to
* clear space for the graphic marking the location.
*/
public int getHorizontalLabelBuffer() {
return horizontalLabelBuffer;
}
// //////////////////////////////////////////////////
// ///////// OMGraphic methods ////////////////////
// //////////////////////////////////////////////////
/**
* Generate the location, and use the declutter matrix to place the label is
* a spot so that it doesn't interset with other labels.
*
* @param proj projection of the map.
* @param declutterMatrix DeclutterMatrix for the map.
*/
public boolean generate(Projection proj, DeclutterMatrix declutterMatrix) {
// Call generate(proj) first, to get the original position
// set. Then, declutter the text.
boolean ret = generate(proj);
if (declutterMatrix != null && label != null && allowDecluttering) {
declutterLabel(declutterMatrix, proj);
}
return ret;
}
/**
* Set the pixel distance that us used by the declutter matrix in trying to
* find a place for the label. If it can't find a place within this pixel
* limit, it wouldn't draw it.
*/
public void setDeclutterLimit(int value) {
if (value < 0 && value != DECLUTTER_LOCALLY) {
declutterLimit = DECLUTTER_ANYWHERE;
} else {
declutterLimit = value;
}
}
/**
* Get the declutter pixel distance limit.
*/
public int getDeclutterLimit() {
return declutterLimit;
}
protected int currentFontDescent = 0;
/**
* Prepare the graphic for rendering. This must be done before calling
* <code>render()</code>! If a vector graphic has lat-lon components, then
* we project these vertices into x-y space. For raster graphics we prepare
* in a different fashion.
* <p>
* If the generate is unsuccessful, it's usually because of some oversight,
* (for instance if <code>proj</code> is null), and if debugging is enabled,
* a message may be output to the controlling terminal.
* <p>
*
* @param proj Projection
* @return boolean true if successful, false if not.
*/
public boolean generate(Projection proj) {
if (label != null) {
label.setY(origYLabelOffset);
label.setX(origXLabelOffset);
}
java.awt.Graphics g = DeclutterMatrix.getGraphics();
if (g != null && label != null) {
g.setFont(label.getFont());
// Now set the vertical offset to the original place based
// off the height of the label, so that the location place
// is halfway up the text. That way, it looks like a
// label.
int height = g.getFontMetrics().getAscent();
currentFontDescent = g.getFontMetrics().getDescent();
label.setX(label.getX() + horizontalLabelBuffer);
label.setY(label.getY() + (height / 2) - 2);
}
if (label != null) {
label.generate(proj);
label.prepareForRender(g);
}
if (location != null)
location.generate(proj);
return true;
}
/**
* Paint the graphic and the name of the location. This should only be used
* if the locations are pretty spread out from each other. If you think you
* need to declutter, you should render all the graphics, and then render
* the names, so that the graphics don't cover up the names.
* <P>
* This paints the graphic into the Graphics context. This is similar to
* <code>paint()</code> function of java.awt.Components. Note that if the
* graphic has not been generated, it will not be rendered. This render will
* take into account the layer showNames and showLocations settings.
*
* @param g Graphics context to render into.
*/
public void render(Graphics g) {
renderLocation(g);
renderName(g);
}
/**
* Paint the graphic label (name) only. This paints the graphic into the
* Graphics context. This is similar to <code>paint()</code> function of
* java.awt.Components. Note that if the graphic has not been generated, it
* will not be rendered. This render will take into account the layer
* showNames and showLocations settings.
*
* @param g Graphics context to render into.
*/
public void renderName(Graphics g) {
if (shouldRenderName()) {
label.render(g);
}
}
/**
* Paint the graphic location graphic only. This paints the graphic into the
* Graphics context. This is similar to <code>paint()</code> function of
* java.awt.Components. Note that if the graphic has not been generated, it
* will not be rendered. This render will take into account the layer
* showNames and showLocations settings.
*
* @param g Graphics context to render into.
*/
public void renderLocation(Graphics g) {
if (shouldRenderLocation()) {
location.render(g);
}
}
/**
* Convenience method to see if handler/global settings dictate that the
* location label should be rendered.
*
* @return true if the name label should be rendered.
*/
protected boolean shouldRenderName() {
boolean globalShowNames = false;
boolean forceGlobal = false;
if (handler != null) {
globalShowNames = handler.isShowNames();
forceGlobal = handler.isForceGlobal();
}
return label != null && ((forceGlobal && globalShowNames) || (!forceGlobal && showName));
}
/**
* Convenience method to see if handler/global settings dictate that the
* location icon should be rendered.
*
* @return true of the location marker should be rendered.
*/
protected boolean shouldRenderLocation() {
boolean globalShowLocations = false;
boolean forceGlobal = false;
if (handler != null) {
globalShowLocations = handler.isShowLocations();
forceGlobal = handler.isForceGlobal();
}
return location != null && ((forceGlobal && globalShowLocations) || (!forceGlobal && showLocation));
}
/**
* Return the shortest distance from the graphic to an XY-point.
*
* @param x X coordinate of the point.
* @param y Y coordinate of the point.
* @return float distance from graphic to the point
*/
public float distance(double x, double y) {
float labelDist = Float.MAX_VALUE;
float locationDist = Float.MAX_VALUE;
if (shouldRenderLocation()) {
locationDist = location.distance(x, y);
}
if (shouldRenderName()) {
labelDist = label.distance(x, y);
}
return (locationDist > labelDist ? labelDist : locationDist);
}
/**
* Given the label is this location has a height and width, find a clean
* place on the map for it. Assumes label is not null.
*
* @param declutter the DeclutterMatrix for the map.
*/
protected void declutterLabel(DeclutterMatrix declutter, Projection proj) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("\nLocation::RepositionText => " + label.getData());
}
// Right now, I think this method takes some presumptuous
// actions, assuming that you want the graphics to take up
// space in the declutter matrix. We might want to delete
// this showLocation code, and let people create their own
// location subclasses that define how the graphic should be
// handled in the declutter matrix.
// I think I will. This stuff is commented out for the
// reasons stated above.
// if (isShowLocation()) {
// Point lp;
// // Take up space with the label
// if (location instanceof OMRasterObject) {
// lp = ((OMRasterObject)location).getMapLocation();
// // This location is the upper left location of the
// // declutter matrix. The declutter matrix works from
// // lower left to upper right.
// if (lp != null) {
// int locHeight = ((OMRasterObject)location).getHeight();
// int locWidth = ((OMRasterObject)location).getWidth();
// // Need to get this right for the DeclutterMatrix
// // space, but changing lp changes where the
// // location will appear - fix this later.
// lp.y += locHeight;
// declutter.setTaken(lp, locWidth, locHeight);
// // Reset it to the original projected location.
// lp.y -= locHeight;
// }
// } else if (renderType != RENDERTYPE_XY) {
// lp = proj.forward(lat,lon);
// lp.x += xOffset-1;
// lp.y += yOffset-1;
// declutter.setTaken(lp, 3, 3);
// } else {
// lp = new Point(x-1, y-1);
// declutter.setTaken(lp, 3, 3);
// }
// }
if (isShowName() || (handler != null && handler.isShowNames())) {
if (label == null || label.getPolyBounds() == null) {
// Why bother going further??
return;
}
Rectangle bounds = label.getPolyBounds().getBounds();
int height = (int) ((float) (bounds.getHeight() - currentFontDescent / 2));
int width = (int) bounds.getWidth();
// Projected location of label on the screen
Point2D p = label.getMapLocation();
if (logger.isLoggable(Level.FINER)) {
logger.finer("old point X Y =>" + p.getX() + " " + p.getY() + " height = " + height + " width = " + width);
}
int limit;
if (declutterLimit == DECLUTTER_LOCALLY) {
limit = height * 2;
} else {
limit = declutterLimit;
}
// newpoint is the new place on the map to put the label
Point2D newpoint = declutter.setNextOpen(p, width, height, limit);
if (logger.isLoggable(Level.FINER)) {
logger.finer("new point X Y =>" + newpoint.getX() + " " + newpoint.getY());
}
label.setMapLocation(newpoint);
}
}
/**
* A simple conversion method for the common String representation of
* decimal degree coordinates, which is a letter denoting the globle
* hemisphere (N or S for latitudes, W or E for longitudes, and then a
* number string. For latitudes, the first two numbers represent the whole
* degree value, and the rest of the numbers represent the fractional
* protion. For longitudes, the first three numbers represent the whole
* degree value. For instance N2443243 equals 24.43243 degrees North, and
* S2443243 results in -24.43243 degrees. Likewise, w12423443 results in
* -124.23443 degrees.
*
* @param coord the coordinate string representing the decimal degree value,
* following the format [NSEW]XXXXXXXXX.
* @return the decimal degrees for the string. There is no notation for you
* to know whether it's a latitude or longitude value.
*/
public static float convertCoordinateString(String coord)
throws NumberFormatException {
float ret = 0f;
String mantissa;
char direction = coord.charAt(0);
if (direction == 'N' || direction == 'S' || direction == 'n' || direction == 's') {
float whole = new Float(coord.substring(1, 3)).floatValue();
ret += whole;
mantissa = coord.substring(3);
} else if (direction == 'W' || direction == 'E' || direction == 'w' || direction == 'e') {
ret += new Float(coord.substring(1, 4)).floatValue();
mantissa = coord.substring(4);
} else {
// Don't know the format!!
throw new NumberFormatException("Location.convertCoordinateString wants <[NSWE]XXXXXXXX>, not getting it.");
}
ret += new Float(mantissa).floatValue() / (float) (Math.pow(10, mantissa.length()));
if (direction == 'W' || direction == 'S' || direction == 'w' || direction == 's') {
ret *= -1f;
}
return ret;
}
/**
* We're using the main function for Location to test the
* convertCoordinateString function.
*/
public static void main(String[] args) {
if (args.length < 1) {
logger.info(" usage: java com.bbn.openmap.layer.location.Location <[NSWE]XXXXXXXX>");
return;
}
float ret = Location.convertCoordinateString(args[0]);
System.out.println(ret);
}
}