// **********************************************************************
//
// <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/proj/Proj.java,v $
// $RCSfile: Proj.java,v $
// $Revision: 1.14 $
// $Date: 2009/01/21 01:24:41 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.proj;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import com.bbn.openmap.util.Debug;
/**
* Proj is the base class of all Projections.
* <p>
* You probably don't want to use this class unless you are hacking your own
* projections, or need extended functionality. To be safe you will want to use
* the Projection interface.
*
* <h3>Notes:</h3>
*
* <ul>
*
* <li>There are no assumption made as to the units of the values provided to
* the projection, each projection type has its own interpretation of what the
* values are.
*
* <li>The order of coordinate parameters changes, depending on the convention
* of the superclass object of the one being referred to. Coordinate tuples
* referring to projected coordinates are generally x, y, those referring to
* unprojected coordinates are y, x (lat/lon).
*
* <li>This class is not thread safe. If two or more threads are using the same
* Proj, then they could disrupt each other. Occasionally you may need to call a
* <code>set</code> method of this class. This might interfere with another
* thread that's using the same projection for <code>forwardPoly</code> or
* another Projection interface method. In general, you should not need to call
* any of the <code>set</code> methods directly, but let the MapBean do it for
* you.
*
* <li>All the various <code>forwardOBJ()</code> methods for ArrayList graphics
* ultimately go through <code>forwardPoly()</code>.
*
* </ul>
*
* @see Cartesian
* @see GeoProj
*
*/
public abstract class Proj implements Projection, Cloneable, Serializable {
/**
* Minimum width of projection.
*/
public final static transient int MIN_WIDTH = 10; // pixels
/**
* Minimum height of projection.
*/
public final static transient int MIN_HEIGHT = 10; // pixels
protected int width = 640, height = 480;
protected double minscale = 1.0; // 1:minscale
protected double maxscale = Double.MAX_VALUE;
protected double scale = maxscale;
protected double centerX;
protected double centerY;
protected String projID = null; // identifies this projection (if needed)
/**
* The rotation angle of the map is stored here so that non-rotating things
* can correct themselves. Is not used for equality checks.
*/
protected double rotationAngle = 0;
/**
* The unprojected coordinates units of measure.
*/
protected Length ucuom = null;
/**
* Construct a projection.
*
* @param center LatLonPoint center of projection
* @param s float scale of projection
* @param w width of screen
* @param h height of screen
* @see ProjectionFactory
*/
public Proj(Point2D center, float s, int w, int h) {
if (Debug.debugging("proj")) {
Debug.output("Proj()");
}
setParms(center, s, w, h);
projID = null;
}
/**
* Set the scale of the projection.
* <p>
* Sets the projection to the scale 1:s iff minscale < s < maxscale.
* <br>
* If s < minscale, sets the projection to minscale. <br>
* If s > maxscale, sets the projection to maxscale. <br>
*
* @param s float scale
*/
public void setScale(float s) {
scale = s;
if (scale < minscale) {
scale = minscale;
} else if (scale > maxscale) {
scale = maxscale;
}
computeParameters();
projID = null;
}
/**
* Set the minscale of the projection.
* <p>
* Usually you will not need to do this.
*
* @param s float minscale
*/
public void setMinScale(float s) {
if (s > maxscale)
return;
minscale = s;
if (scale < minscale) {
scale = minscale;
}
computeParameters();
projID = null;
}
/**
* Set the maximum scale of the projection.
* <p>
* Usually you will not need to do this.
*
* @param s float minscale
*/
public void setMaxScale(float s) {
if (s < minscale)
return;
maxscale = s;
if (scale > maxscale) {
scale = maxscale;
}
computeParameters();
projID = null;
}
/**
* Get the scale of the projection.
*
* @return double scale value
*/
public float getScale() {
return (float) scale;
}
/**
* Get the maximum scale of the projection.
*
* @return double max scale value
*/
public float getMaxScale() {
return (float) maxscale;
}
/**
* Get minimum scale of the projection.
*
* @return double min scale value
*/
public float getMinScale() {
return (float) minscale;
}
/**
* Set center point of projection.
*
* @param pt Point2D for center. Point2D values will be copied.
*/
public void setCenter(Point2D pt) {
setCenter(pt.getY(), pt.getX());
}
/**
* Set center point of projection.
*
* @param y vertical value of center.
* @param x horizontal value of center.
*/
public void setCenter(double y, double x) {
// Since we need to re-run computeParameters and the projID,
// we better make a clone of the center, so whoever sets the
// center can't change it by simply changing the center's
// parameters without recalculating the parameters.
centerX = x;
centerY = y;
computeParameters();
projID = null;
}
/**
* Get center point of projection.
*
* @return Point2D center of projection, created just for you.
*/
public Point2D getCenter() {
return getCenter(new Point2D.Double());
}
/**
* Returns a center Point2D that was provided, with the location filled into
* the Point2D object. Calls Point2D.setLocation(x, y).
*/
public <T extends Point2D> T getCenter(T center) {
center.setLocation(centerX, centerY);
return center;
}
/**
* Set projection width.
*
* @param width width of projection screen
*/
public void setWidth(int width) {
this.width = width;
if (this.width < MIN_WIDTH) {
Debug.message("proj", "Proj.setWidth: width too small!");
this.width = MIN_WIDTH;
}
computeParameters();
projID = null;
}
/**
* Set projection height.
*
* @param height height of projection screen
*/
public void setHeight(int height) {
this.height = height;
if (this.height < MIN_HEIGHT) {
Debug.message("proj", "Proj.setHeight: height too small!");
this.height = MIN_HEIGHT;
}
computeParameters();
projID = null;
}
/**
* Get projection width.
*
* @return width of projection screen
*/
public int getWidth() {
return width;
}
/**
* Get projection height.
*
* @return height of projection screen
*/
public int getHeight() {
return height;
}
/**
* Sets all the projection variables at once before calling
* computeParameters().
*
* @param center LatLonPoint center
* @param scale float scale
* @param width width of screen
* @param height height of screen
*/
protected void setParms(Point2D center, float scale, int width, int height) {
centerX = center.getX();
centerY = center.getY();
this.scale = scale;
this.width = width;
if (this.width < MIN_WIDTH) {
Debug.message("proj", "Proj.setParms: width too small!");
this.width = MIN_WIDTH;
}
this.height = height;
if (this.height < MIN_HEIGHT) {
Debug.message("proj", "Proj.setParms: height too small!");
this.height = MIN_HEIGHT;
}
init();
if (this.scale < minscale) {
this.scale = minscale;
} else if (this.scale > maxscale) {
this.scale = maxscale;
}
computeParameters();
projID = null;
}
/**
* Called after the center and scale is set in setParams, but before the
* scale is checked for legitimacy. This is an opportunity to set constants
* in subclasses before anything else gets called or checked for validity.
* This is different than computeParameters() which is called after some
* checks. This is a good time to pre-calculate constants and set maxscale
* and minscale.
* <P>
* Make sure you call super.init() if you override this method.
*/
protected void init() {
}
/**
* Sets the projection ID used for determining equality. The projection ID
* String is interned for efficient comparison.
*/
protected void setProjectionID() {
projID = (getClass().getName() + ":" + scale + ":" + centerX + ":" + centerY + ":" + width
+ ":" + height + ":" + rotationAngle);
}
/**
* Gets the projection ID used for determining equality.
*
* @return the projection ID, as an intern()ed String
*/
public String getProjectionID() {
if (projID == null)
setProjectionID();
return projID;
}
/**
* Called when some fundamental parameters change.
* <p>
* Each projection will decide how to respond to this change. For instance,
* they may need to recalculate "constant" parameters used in the forward()
* and inverse() calls.
*/
protected abstract void computeParameters();
/**
* Stringify the projection.
*
* @return stringified projection
* @see #getProjectionID
*/
public String toString() {
return (" center(" + centerX + ":" + centerY + ") scale=" + scale + " maxscale=" + maxscale
+ " minscale=" + minscale + " width=" + width + " height=" + height + "]");
}
/**
* Test for equality.
*
* @param o Object to compare.
* @return boolean comparison
*/
public boolean equals(Object o) {
if (o == null)
return false;
if (getClass() != o.getClass()) {
return false;
}
return getProjectionID().equals(((Projection) o).getProjectionID());
}
/**
* Return hashcode value of projection.
*
* @return int hashcode
*/
public int hashCode() {
return getProjectionID().hashCode();
}
/**
* Clone the projection.
*
* @return Projection clone of this one.
*/
public Projection makeClone() {
return (Projection) this.clone();
}
/**
* Copies this projection.
*
* @return a copy of this projection.
*/
public Object clone() {
try {
return (Proj) super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
}
/**
* Forward project a LatLonPoint.
* <p>
* Forward projects a LatLon point into XY space. Returns a Point2D.
*
* @param llp LatLonPoint to be projected
* @return Point2D (new)
*/
public Point2D forward(Point2D llp) {
return forward(llp.getY(), llp.getX(), new Point2D.Float());
}
/**
* Forward projects a LatLonPoint into XY space and return a
* java.awt.geom.Point2D.
*
* @param llp LatLonPoint to be projected
* @param pt Resulting XY Point2D
* @return Point2D pt
*/
public Point2D forward(Point2D llp, Point2D pt) {
return forward(llp.getY(), llp.getX(), pt);
}
/**
* Forward project lat,lon coordinates.
*
* @param lat float latitude in decimal degrees
* @param lon float longitude in decimal degrees
* @return Point2D (new)
*/
public Point2D forward(float lat, float lon) {
return forward((double) lat, (double) lon, new Point2D.Float());
}
public Point2D forward(float lat, float lon, Point2D pt) {
return forward((double) lat, (double) lon, pt);
}
public Point2D forward(double lat, double lon) {
return forward(lat, lon, new Point2D.Double());
}
public abstract Point2D forward(double lat, double lon, Point2D pt);
public <T extends Point2D> T inverse(Point2D point, T llpt) {
return inverse(point.getX(), point.getY(), llpt);
}
/**
* Inverse project a Point2D from x,y space to coordinate space.
*
* @param point x,y Point2D
* @return LatLonPoint (new)
*/
public Point2D inverse(Point2D point) {
return inverse(point.getX(), point.getY(), new Point2D.Double());
}
/**
* Inverse project x,y coordinates.
*
* @param x integer x coordinate
* @param y integer y coordinate
* @return LatLonPoint (new)
* @see #inverse(Point2D)
*/
public Point2D inverse(double x, double y) {
return inverse(x, y, new Point2D.Double());
}
public abstract <T extends Point2D> T inverse(double x, double y, T llpt);
/**
* Simple shape inverse projection, converts the x,y values in the shape to
* the x, y values of the projection.
*
* @param shape projected shape.
* @return Shape containing source coordinates inversely projected.
*/
public Shape inverseShape(Shape shape) {
PathIterator pi = shape.getPathIterator(null);
double[] coords = new double[6];
Point2D world = new Point2D.Double();
Point2D world2 = new Point2D.Double();
Point2D world3 = new Point2D.Double();
Point2D screen = new Point2D.Double();
Point2D screen2 = new Point2D.Double();
Point2D screen3 = new Point2D.Double();
GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
while (!pi.isDone()) {
int type = pi.currentSegment(coords);
screen.setLocation(coords[0], coords[1]);
inverse(screen, world);
if (type == PathIterator.SEG_MOVETO) {
path.moveTo(world.getX(), world.getY());
} else if (type == PathIterator.SEG_LINETO) {
path.lineTo(world.getX(), world.getY());
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
} else {
screen2.setLocation(coords[2], coords[3]);
inverse(screen2, world2);
if (type == PathIterator.SEG_QUADTO) {
path.quadTo(world.getX(), world.getY(), world2.getX(), world2.getY());
} else if (type == PathIterator.SEG_CUBICTO) {
screen3.setLocation(coords[4], coords[5]);
inverse(screen3, world3);
path.curveTo(world.getX(), world.getY(), world2.getX(), world2.getY(), world3.getX(), world3.getY());
}
}
pi.next();
}
return path;
}
/**
* Simple shape projection, doesn't take into account what kind of lines
* should be drawn between shape points, assumes they should be 2D lines as
* rendered in 2D space, not interpolated for accuracy as Great Circle/Rhumb
* lines on a globe..
*/
public Shape forwardShape(Shape shape) {
PathIterator pi = shape.getPathIterator(null);
double[] coords = new double[6];
Point2D world = new Point2D.Double();
Point2D world2 = new Point2D.Double();
Point2D world3 = new Point2D.Double();
Point screen = new Point();
Point screen2 = new Point();
Point screen3 = new Point();
GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
while (!pi.isDone()) {
int type = pi.currentSegment(coords);
world.setLocation(coords[0], coords[1]);
forward(world, screen);
if (type == PathIterator.SEG_MOVETO) {
path.moveTo(screen.x, screen.y);
} else if (type == PathIterator.SEG_LINETO) {
path.lineTo(screen.x, screen.y);
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
} else {
world2.setLocation(coords[2], coords[3]);
forward(world2, screen2);
if (type == PathIterator.SEG_QUADTO) {
path.quadTo(screen.x, screen.y, screen2.x, screen2.y);
} else if (type == PathIterator.SEG_CUBICTO) {
world3.setLocation(coords[4], coords[5]);
forward(world3, screen3);
path.curveTo(screen.x, screen.y, screen2.x, screen2.y, screen3.x, screen3.y);
}
}
pi.next();
}
return path;
}
/**
* Forward project a raw array of points. This assumes nothing about the
* array of coordinates. In no way does it assume the points are connected
* or that the composite figure is to be filled.
* <p>
* It does populate a visible array indicating whether the points are
* visible on the projected view of the world.
* <p>
*
* @param rawllpts array of y, x world coordinates.
* @param rawoff offset into rawllpts.
* @param xcoords x projected horizontal map coordinates.
* @param ycoords y projected vertical map coordinates.
* @param visible coordinates visible?
* @param copyoff offset into x,y visible arrays.
* @param copylen number of coordinates (coordinate arrays should be at
* least this long, rawllpts should be at least twice as long).
* @return boolean true if all points visible, false if some points not
* visible.
*/
public boolean forwardRaw(float[] rawllpts, int rawoff, float[] xcoords, float[] ycoords,
boolean[] visible, int copyoff, int copylen) {
Point temp = new Point();
int end = copylen + copyoff;
for (int i = copyoff, j = rawoff; i < end; i++, j += 2) {
forward(rawllpts[j], rawllpts[j + 1], temp);
xcoords[i] = temp.x;
ycoords[i] = temp.y;
visible[i] = true;
}
return true;
}
/**
* Forward project a raw array of points. This assumes nothing about the
* array of coordinates. In no way does it assume the points are connected
* or that the composite figure is to be filled.
* <p>
* It does populate a visible array indicating whether the points are
* visible on the projected view of the world.
* <p>
*
* @param rawllpts array of y, x world coordinates.
* @param rawoff offset into rawllpts.
* @param xcoords x projected horizontal map coordinates.
* @param ycoords y projected vertical map coordinates.
* @param visible coordinates visible?
* @param copyoff offset into x,y visible arrays.
* @param copylen number of coordinates (coordinate arrays should be at
* least this long, rawllpts should be at least twice as long).
* @return boolean true if all points visible, false if some points not
* visible.
*/
public boolean forwardRaw(double[] rawllpts, int rawoff, float[] xcoords, float[] ycoords,
boolean[] visible, int copyoff, int copylen) {
Point temp = new Point();
int end = copylen + copyoff;
for (int i = copyoff, j = rawoff; i < end; i++, j += 2) {
forward(rawllpts[j], rawllpts[j + 1], temp);
xcoords[i] = temp.x;
ycoords[i] = temp.y;
visible[i] = true;
}
return true;
}
public ArrayList<float[]> forwardLine(Point2D ll1, Point2D ll2) {
double[] rawllpts = { ll1.getY(), ll1.getX(), ll2.getY(), ll2.getX() };
return forwardPoly(rawllpts, false);
}
/**
* Forward project a rectangle.
*
* @param ll1 LatLonPoint
* @param ll2 LatLonPoint
* @return ArrayList<float[]>
*/
public ArrayList<float[]> forwardRect(Point2D ll1, Point2D ll2) {
double[] rawllpts = { ll1.getY(), ll1.getX(), ll1.getY(), ll2.getX(), ll2.getY(),
ll2.getX(), ll2.getY(), ll1.getX(),
// connect:
ll1.getY(), ll1.getX() };
return forwardPoly(rawllpts, true);
}
public ArrayList<float[]> forwardPoly(float[] rawllpts, boolean isFilled) {
// For regular OMGraphics, some of the rawllpts are in radians and must
// be translated into decimal degrees before they really are able to be
// displayed here, i.e.:
// wx = Math.toDegrees(wx);
// wy = Math.toDegrees(wy);
int n, k;
// determine length of pairs list
int len = rawllpts.length >> 1; // len/2, chop off extra
if (len < 2)
return new ArrayList<float[]>(0);
// determine when to stop
Point temp = new Point(0, 0);
float[] xs = new float[len];
float[] ys = new float[len];
// forward project the first point
forward(rawllpts[0], rawllpts[1], temp);
xs[0] = temp.x;
ys[0] = temp.y;
// forward project the other points
for (n = 1, k = 2; n < len; n++, k += 2) {
forward(rawllpts[k], rawllpts[k + 1], temp);
xs[n] = temp.x;
ys[n] = temp.y;
}
// now create the return list
ArrayList<float[]> ret_val = null;
ret_val = new ArrayList<float[]>(2);
ret_val.add(xs);
ret_val.add(ys);
return ret_val;
}
public ArrayList<float[]> forwardPoly(double[] rawllpts, boolean isFilled) {
int n, k;
// determine length of pairs list
int len = rawllpts.length >> 1; // len/2, chop off extra
if (len < 2)
return new ArrayList<float[]>(0);
// determine when to stop
Point temp = new Point(0, 0);
float[] xs = new float[len];
float[] ys = new float[len];
// forward project the first point
forward(rawllpts[0], rawllpts[1], temp);
xs[0] = temp.x;
ys[0] = temp.y;
// forward project the other points
for (n = 1, k = 2; n < len; n++, k += 2) {
forward(rawllpts[k], rawllpts[k + 1], temp);
xs[n] = temp.x;
ys[n] = temp.y;
}
// now create the return list
ArrayList<float[]> ret_val = null;
ret_val = new ArrayList<float[]>(2);
ret_val.add(xs);
ret_val.add(ys);
return ret_val;
}
/**
* Pan the map/projection.
* <p>
* Example pans:
* <ul>
* <li><code>pan(180, c)</code> pan south `c' degrees
* <li><code>pan(-90, c)</code> pan west `c' degrees
* <li><code>pan(0, c)</code> pan north `c' degrees
* <li><code>pan(90, c)</code> pan east `c' degrees
* </ul>
*
* @param Az azimuth "east of north" in decimal degrees:
* <code>-180 <= Az <= 180</code>
* @param c arc distance in decimal degrees
*/
abstract public void pan(double Az, double c);
/**
* Pan the map/projection.
* <ul>
* <li><code>pan(180, c)</code> pan south
* <li><code>pan(-90, c)</code> pan west
* <li><code>pan(0, c)</code> pan north
* <li><code>pan(90, c)</code> pan east
* </ul>
*
* @param Az azimuth "east of north" in decimal degrees:
* <code>-180 <= Az <= 180</code>
*/
abstract public void pan(double Az);
/**
* pan the map northwest.
*/
final public void panNW() {
pan(-45f);
}
final public void panNW(double c) {
pan(-45f);
}
/**
* pan the map north.
*/
final public void panN() {
pan(0f);
}
final public void panN(double c) {
pan(0f);
}
/**
* pan the map northeast.
*/
final public void panNE() {
pan(45f);
}
final public void panNE(double c) {
pan(45f);
}
/**
* pan the map east.
*/
final public void panE() {
pan(90f);
}
final public void panE(double c) {
pan(90f);
}
/**
* pan the map southeast.
*/
final public void panSE() {
pan(135f);
}
final public void panSE(double c) {
pan(135f);
}
/**
* pan the map south.
*/
final public void panS() {
pan(180f);
}
final public void panS(double c) {
pan(180f);
}
/**
* pan the map southwest.
*/
final public void panSW() {
pan(-135f);
}
final public void panSW(double c) {
pan(-135f);
}
/**
* pan the map west.
*/
final public void panW() {
pan(-90f);
}
final public void panW(double c) {
pan(-90f);
}
/**
*/
public boolean isPlotable(double lat, double lon) {
return isPlotable(lat, lon);
}
public boolean isPlotable(Point2D point) {
return isPlotable(point.getY(), point.getX());
}
/**
* Draw the background for the projection.
*
* @param g Graphics2D
* @param paint java.awt.Paint to use for the background
*/
public void drawBackground(Graphics2D g, java.awt.Paint paint) {
g.setPaint(paint);
drawBackground(g);
}
/**
* Draw the background for the projection. Assume that the Graphics has been
* set with the Paint/Color needed, just render the shape of the background.
*
* @param g Graphics
*/
public void drawBackground(Graphics g) {
g.fillRect(0, 0, getWidth(), getHeight());
}
/**
* Get the name string of the projection.
*/
public String getName() {
return "Proj";
}
/**
* Given a couple of points representing a bounding box, find out what the
* scale should be in order to make those points appear at the corners of
* the projection.
*
* @param ll1 the upper left coordinates of the bounding box.
* @param ll2 the lower right coordinates of the bounding box.
* @param point1 a java.awt.geom.Point2D reflecting a pixel spot on the
* projection that matches the ll1 coordinate, the upper left corner
* of the area of interest. Note that this is the location where you
* want ll1 to go in the new projection scale, not where it is now.
* @param point2 a java.awt.geom.Point2D reflecting a pixel spot on the
* projection that matches the ll2 coordinate, usually the lower
* right corner of the area of interest. Note that this is the
* location where you want ll2 to go in the new projection, not where
* it is now.
*/
public abstract float getScale(Point2D ll1, Point2D ll2, Point2D point1, Point2D point2);
/**
* Overridden to ensure that setParameters() are called with the read
* values.
*
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
computeParameters();
projID = null;
}
/**
* Get the unprojected coordinates units of measure.
*
* @return Length. May be null if unknown.
* @see Length
*/
public Length getUcuom() {
return ucuom;
}
/**
* Set the unprojected coordinates units of measure.
*
* @param ucuom
*/
public void setUcuom(Length ucuom) {
this.ucuom = ucuom;
}
/**
* Get the world coordinate of the upper left corner of the map.
*/
public Point2D getUpperLeft() {
return inverse(0, 0, new Point2D.Double());
}
/**
* Get the world coordinate of the lower right corner of the map.
*/
public Point2D getLowerRight() {
return inverse(width, height, new Point2D.Double());
}
/**
* @return the rotationAngle
*/
public double getRotationAngle() {
return rotationAngle;
}
/**
* This setting is purely for informational purposes, as a way for the
* projection to pass along any rotation activity of the MapBean to
* OMGraphics. Setting this value will not rotate the map. Rotating the map
* should be done directly to the MapBean.
*
* @param rotationAngle the rotationAngle to set, in RADIANS
*/
public void setRotationAngle(double rotationAngle) {
this.rotationAngle = rotationAngle;
}
}