// ********************************************************************** // // <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; } }