// ********************************************************************** // // <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/omGraphics/geom/BasicGeometry.java,v $ // $RCSfile: BasicGeometry.java,v $ // $Revision: 1.19 $ // $Date: 2009/01/21 01:24:42 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap.omGraphics.geom; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.FlatteningPathIterator; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.PathIterator; import java.io.Serializable; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import com.bbn.openmap.omGraphics.OMGeometry; import com.bbn.openmap.omGraphics.OMGraphicConstants; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.util.Debug; /** * Base class implementation of OpenMap OMGeometry, the super class for all * OMGraphics. * <p> * * The geometry classes are intended to pull the object location data out of the * OMGraphics. If you have a bunch of OMGraphics that are all rendered with * common attributes, you can create a bunch of OMGeometry objects to plavce in * a OMGeometryList that will render them all alike. * <p> * * The BasicGeometry can hold attributes. Traditionally, there has been an * appObject (Application Object) that could be set in the OMGeometry/OMGraphic * to maintain a pointer for additional information about the shape. This has * been modified so that an attribute Map can be maintained for the * BasicGeometry to let it hold on to a bunch of organized attributes. To * maintain backward compatibility, the setAppObject() and getAppObject() * methods have been modified to manage a java.util.Map along with any Objects * stored in the appObject. Using the setAppObject() and getAppObject() methods * in conjunction with other attributes will cause that object to be stored in * the attribute Map under the APP_OBJECT_KEY Map key. * * @see PolygonGeometry * @see PolylineGeometry * @see com.bbn.openmap.omGraphics.OMGeometryList * @see Projection */ public abstract class BasicGeometry implements OMGeometry, Serializable, OMGraphicConstants { /** * The lineType describes the way a line will be drawn between points. * LINETYPE_STRAIGHT will mean the line is drawn straight between the pixels * of the endpoints of the line, across the window. LINETYPE_GREATCIRCLE * means the line will be drawn on the window representing the shortest line * along the land. LINETYPE_RHUMB means a line will be drawn along a * constant bearing between the two points. */ protected int lineType = LINETYPE_UNKNOWN; /** Flag to indicate that the object needs to be re-projected. */ protected transient boolean needToRegenerate = true; /** * Attribute Map for this Geometry/OMGraphic. */ protected Map<Object, Object> attributes; /** * A flag to render this geometry visible. */ protected boolean visible = true; /** * The Java 2D containing the Shape of the Graphic. There may be several * paths appended to each other, in case the graphic wraps around the earth, * and we need to show the other edge of the graphic on the other side of * the earth. */ protected transient GeneralPath shape = null; // //////////////////////////////////////////////////////// /** * Set the line type for the graphic, which will affect how the lines will * be drawn. See the definition of the lineType parameter. Accepts * LINETYPE_RHUMB, LINETYPE_STRAIGHT and LINETYPE_GREATCIRCLE. Any weird * values get set to LINETYPE_STRAIGHT. * * @param value the line type of the graphic. */ public void setLineType(int value) { if (lineType == value) return; setNeedToRegenerate(true); // flag dirty lineType = value; } /** * Return the line type. * * @return the linetype - LINETYPE_RHUMB, LINETYPE_STRAIGHT, * LINETYPE_GREATCIRCLE or LINETYPE_UNKNOWN. */ public int getLineType() { return lineType; } /** * Return the render type. * * @return the rendertype of the object - RENDERTYPE_LATLON, RENDERTYPE_XY, * RENDERTYPE_OFFSET and RENDERTYPE_UNKNOWN. */ public abstract int getRenderType(); /** * Sets the regenerate flag for the graphic. This flag is used to determine * if extra work needs to be done to prepare the object for rendering. This * also sets the shape to null; * * @param value boolean */ public void setNeedToRegenerate(boolean value) { needToRegenerate = value; if (value == true) { setShape(null); } } /** * Return the regeneration status. * * @return boolean */ public boolean getNeedToRegenerate() { return needToRegenerate; } /** * Set the visibility variable. NOTE: <br> * This is checked by the OMGeometryList when it iterates through its list * for render and gesturing. It is not checked by the internal OMGeometry * methods, although maybe it should be... * * @param visible boolean */ public void setVisible(boolean visible) { this.visible = visible; } /** * Get the visibility variable. * * @return boolean */ public boolean isVisible() { return visible; } /** * Let the geometry object know it's selected. No action mandated. */ public void select() { } /** * Let the geometry object know it's deselected. No action mandated. */ public void deselect() { } /** * Method to extend if you don't like Hashtables used for attribute table. */ protected Map<Object, Object> createAttributeMap() { return Collections.synchronizedMap(new LinkedHashMap<Object, Object>()); } /** * Adds a key-value pair to the attribute Map. The Map will be created if it * doesn't exist. */ public void putAttribute(Object key, Object value) { if (key != null) { getAttributes().put(key, value); } } /** * Returns the object stored in a Map stored in the appObject. If the * appObject is a Map, the key will be passed to it even if the Map isn't * considered to be the 'official' attribute Map. */ public Object getAttribute(Object key) { if (key != null) { return getAttributes().get(key); } return null; } /** * Removes the object stored in a Map stored in the appObject. If the * appObject is a Map, the key will be passed to it even if the Map isn't * considered to be the 'official' attribute Map. Returns the removed value * from the Map, or null if there wasn't a value for the given key. */ public Object removeAttribute(Object key) { if (key != null) { return getAttributes().remove(key); } return null; } /** * Removes all of the objects stored in a Map stored in the appObject. If * the appObject is a Map, the clear command will be passed to it even if * the Map isn't considered to be the 'official' attribute Map. */ public void clearAttributes() { getAttributes().clear(); } /** * Returns the 'official' attribute Map. */ public Map<Object, Object> getAttributes() { if (attributes == null) { attributes = createAttributeMap(); } return attributes; } /** * Sets the 'official' attribute Map, moving any appObject that isn't * currently the 'official' attribute Map into the map under the * APP_OBJECT_KEY. */ public void setAttributes(Map<Object, Object> atts) { attributes = atts; } /** * For backward compatibility, puts object in attribute table under * OMGraphic.APP_OBJECT. * * @param obj any object */ public void setAppObject(Object obj) { putAttribute(APP_OBJECT, obj); } /** * For backward compatibility, now returns whatever is saved under the * OMGraphic.APP_OBJECT key. * * @return Object */ public Object getAppObject() { return getAttribute(APP_OBJECT); } /** * OMGeometry method for returning a simple description of the OMGraphic. */ public String getDescription() { String cname = getClass().getName(); int lastPeriod = cname.lastIndexOf('.'); if (lastPeriod != -1) { cname = cname.substring(lastPeriod + 1); } return cname; } // //////////////////////////////////////////////////////////////////////// /** * Prepare the geometry 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. * @see #regenerate */ public abstract boolean generate(Projection proj); /** * A check to see if the OMGeometry is ready for rendering. Calls getShape() * to pass to other isRenderable() method. */ public boolean isRenderable() { return isRenderable(getShape()); } /** * A check to see if the OMGeometry is ready for rendering. This is the * method you should call, with a the handle to the shape object you're * interested in rendering. * * @param shape the projected shape of the OMGraphic * @return true if draw/fill should be called. */ public boolean isRenderable(Shape shape) { return (!getNeedToRegenerate() && isVisible() && shape != null); } /** * Paints the graphic, as a filled shape. The Graphics object should be set * for rendering. Calls the getShape() method, which is synchronized, before * calling the other fill method. It's better to call the other one after * doing renderable check, and then call draw, with a single handle to the * current shape object. * <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 or if it isn't visible, it will not be * rendered. * <P> * * This method used to be abstract, but with the conversion of OMGeometrys * to internally represent themselves as java.awt.Shape objects, it's a more * generic method. If the OMGeometry hasn't been updated to use Shape * objects, it should have its own render method. * * @param g Graphics2D context to render into. */ public void fill(Graphics g) { fill(g, getShape()); } /** * Paint the graphic, as a filled shape. The Graphics object should be set * for rendering. * <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 or if it isn't visible, it will not be * rendered. * <P> * * This method used to be abstract, but with the conversion of OMGeometrys * to internally represent themselves as java.awt.Shape objects, it's a more * generic method. If the OMGeometry hasn't been updated to use Shape * objects, it should have its own render method. * * @param g Graphics2D context to render into. * @param s Shape object to fill for rendering. */ public void fill(Graphics g, Shape s) { if (s != null) { ((Graphics2D) g).fill(s); } } /** * Paint the graphic, as an outlined shape. The Graphics object should be * ready for rendering (paint, stroke). * <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 or if it isn't visible, it will not be * rendered. * <P> * * This method used to be abstract, but with the conversion of OMGeometrys * to internally represent themselves as java.awt.Shape objects, it's a more * generic method. If the OMGeometry hasn't been updated to use Shape * objects, it should have its own render method. * * @param g Graphics2D context to render into. * @param s Shape object to render. */ public void draw(Graphics g, Shape s) { if (s != null) { ((Graphics2D) g).draw(s); } } /** * Paint the graphic, as an outlined shape. The Graphics object should be * ready for rendering (paint, stroke). Calls the getShape() method, which * is synchronized, before calling the other fill method. It's better to * call the other one after doing renderable check and fill, with a single * handle to the current shape object. * <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 or if it isn't visible, it will not be * rendered. * <P> * * This method used to be abstract, but with the conversion of OMGeometrys * to internally represent themselves as java.awt.Shape objects, it's a more * generic method. If the OMGeometry hasn't been updated to use Shape * objects, it should have its own render method. * * @param g Graphics2D context to render into. */ public void draw(Graphics g) { draw(g, getShape()); } /** * Sure it renders, but not with any controllable rendering attributes - the * colors and strokes are what are set in the Graphics. The OMGeometryList * controls this better, this method shouldn't really be called directly. */ public void render(Graphics g) { Shape s = getShape(); if (isRenderable(s)) { fill(g, s); draw(g, s); } } /** * Return the shortest distance from the edge of a graphic to an XY-point. * <p> * * @param x X coordinate of the point. * @param y Y coordinate of the point. * @return float distance, in pixels, from graphic to the point. Returns * Float.POSITIVE_INFINITY if the graphic isn't ready (ungenerated). */ public float distanceToEdge(double x, double y) { float distance = Float.POSITIVE_INFINITY; GeneralPath shape = getShape(); if (!getNeedToRegenerate() && shape != null) { distance = BasicGeometry.distanceToEdge(x, y, shape); } return distance; } /** * Return the shortest distance from the edge of a Shape object to an * XY-point. * <p> * Method taken and adapted from * {@link BasicGeometry#distanceToEdge(double, double)} * * @param x X coordinate of the point. * @param y Y coordinate of the point. * @param shape Shape object to test. * @return float distance, in pixels, from Shape to the point. Returns * Float.POSITIVE_INFINITY if the Shape is null. */ public static float distanceToEdge(final double x, final double y, final Shape shape) { float distance = Float.POSITIVE_INFINITY; if (shape == null) { return distance; } final PathIterator pi2 = shape.getPathIterator(null); final FlatteningPathIterator pathIt = new FlatteningPathIterator(pi2, .25); final double[] coords = new double[6]; double endPntX = Double.NaN; double endPntY = Double.NaN; double lastMovedToPntX = Double.NaN; double lastMovedToPntY = Double.NaN; while (!pathIt.isDone()) { final int type = pathIt.currentSegment(coords); if (type == PathIterator.SEG_LINETO) { final double startPntX = endPntX; final double startPntY = endPntY; endPntX = coords[0]; endPntY = coords[1]; final float dist = (float) Line2D.ptSegDist(startPntX, startPntY, endPntX, endPntY, x, y); if (dist < distance) { distance = dist; } } else if (type == PathIterator.SEG_MOVETO) { endPntX = coords[0]; endPntY = coords[1]; lastMovedToPntX = coords[0]; lastMovedToPntY = coords[1]; } else if (type == PathIterator.SEG_CLOSE) { final double startPntX = lastMovedToPntX; final double startPntY = lastMovedToPntY; endPntX = coords[0]; endPntY = coords[1]; final float dist = (float) Line2D.ptSegDist(startPntX, startPntY, endPntX, endPntY, x, y); if (dist < distance) { distance = dist; } } pathIt.next(); } return distance; } /** * Return the shortest distance from the graphic to an XY-point. Checks to * see of the point is contained within the OMGraphic, which may, or may not * be the right thing for clear OMGraphics or lines. * <p> * * This method used to be abstract, but with the conversion of OMGeometrys * to internally represent themselves as java.awt.Shape objects, it's a more * generic method. If the OMGeometry hasn't been updated to use Shape * objects, it should have its own distance method. * <p> * * Calls _distance(x, y); * * @param x X coordinate of the point. * @param y Y coordinate of the point. * @return float distance, in pixels, from graphic to the point. Returns * Float.POSITIVE_INFINITY if the graphic isn't ready (ungenerated). */ public float distance(double x, double y) { return _distance(x, y); } /** * Return the shortest distance from the graphic to an XY-point. Checks to * see of the point is contained within the OMGraphic, which may, or may not * be the right thing for clear OMGraphics or lines. * <p> * * _distance was added so subclasses could make this call if their * geometries/attributes require this action (when fill color doesn't * matter). * * @param x X coordinate of the point. * @param y Y coordinate of the point. * @return float distance, in pixels, from graphic to the point. Returns * Float.POSITIVE_INFINITY if the graphic isn't ready (ungenerated). */ protected float _distance(double x, double y) { float distance = Float.POSITIVE_INFINITY; GeneralPath shape = getShape(); if (getNeedToRegenerate() || shape == null) { return distance; } if (shape.contains(x, y)) { // if (Debug.debugging("omgraphicdetail")) { // Debug.output(" contains " + x + ", " + y); // } return 0f; } else { return distanceToEdge(x, y); } } /** * Answers the question whether or not the OMGeometry contains the given * pixel point. * <P> * This method used to be abstract, but with the conversion of OMGeometrys * to internally represent themselves as java.awt.Shape objects, it's a more * generic method. If the OMGeometry hasn't been updated to use Shape * objects, it should have its own contains method. * <P> * This method duplicates a java.awt.Shape method, with some protection * wrapped around it. If you have other queries for the internal Shape * object, just ask for it and then ask it directly. This method is provided * because it is the most useful, used when determining if a mouse event is * occurring over an object on the map. * * @param x X pixel coordinate of the point. * @param y Y pixel coordinate of the point. * @return getShape().contains(x, y), false if the OMGraphic hasn't been * generated yet. */ public boolean contains(double x, double y) { Shape shape = getShape(); boolean ret = false; if (shape != null) { ret = shape.contains(x, y); } return ret; } /** * Invoke this to regenerate a "dirty" graphic. This method is a wrapper * around the <code>generate()</code> method. It invokes * <code>generate()</code> only if<code> needToRegenerate() </code> on the * graphic returns true. To force a graphic to be generated, call * <code>generate()</code> directly. * * @param proj the Projection * @return true if generated, false if didn't do it (maybe a problem). * @see #generate */ public boolean regenerate(Projection proj) { if (proj == null) { return false; } if (getNeedToRegenerate()) { return generate(proj); } return false; } /** * Get the java.awt.Shape object that represents the projected graphic. The * array will one Shape object even if the object wraps around the earth and * needs to show up in more than one place on the map. In conditions like * that, the Shape will have multiple parts. * <p> * * The java.awt.Shape object gives you the ability to do a little spatial * analysis on the graphics. * * @return java.awt.geom.GeneralPath (a java.awt.Shape object), or null if * the graphic needs to be generated with the current map * projection, or null if the OMGeometry hasn't been updated to use * Shape objects for its internal representation. */ public synchronized GeneralPath getShape() { return shape; } /** * Set the java.awt.Shape object that represents the projected graphic. This * Shape object should be internally generated, but this method is provided * to clear out the object to save memory, or to allow a little * customization if your requirements dictate. * <p> * * The java.awt.Shape object gives you the ability to do a little spatial * analysis on the graphics. * * @param gp java.awt.geom.GeneralPath, or null if the graphic needs to be * generated with the current map projection or to clear out the * object being held by the OMGeometry. */ public synchronized void setShape(GeneralPath gp) { shape = gp; } /** * Create a Shape object given an array of x points and y points. The x * points a y points should be projected. This method is used by subclasses * that get projected coordinates out of the projection classes, and they * need to build a Shape object from those coordinates. * * @param xpoints projected x coordinates * @param ypoints projected y coordinates * @param isPolygon whether the points make up a polygon, or a polyline. If * it's true, the Shape object returned is a Polygon. If false, the * Shape returned is a GeneralPath object. * @return The Shape object for the points. */ public static GeneralPath createShape(float[] xpoints, float[] ypoints, boolean isPolygon) { return createShape(xpoints, ypoints, 0, xpoints.length, isPolygon); } /** * Create a Shape object given an array of x points and y points. The x * points a y points should be projected. This method is used by subclasses * that get projected coordinates out of the projection classes, and they * need to build a Shape object from those coordinates. * * @param xpoints projected x coordinates * @param ypoints projected y coordinates * @param startIndex the starting coordinate index in the array. * @param length the number of points to use from the array for the shape. * @param isPolygon whether the points make up a polygon, or a polyline. If * it's true, the Shape object returned is a Polygon. If false, the * Shape returned is a GeneralPath object. * @return The Shape object for the points. */ public static GeneralPath createShape(float[] xpoints, float[] ypoints, int startIndex, int length, boolean isPolygon) { // used to return a Shape if (xpoints == null || ypoints == null) { return null; } if (startIndex < 0) { startIndex = 0; } if (length > xpoints.length - startIndex) { // Do as much as you can... length = xpoints.length - startIndex - 1; } GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, length); if (length > startIndex) { path.moveTo(xpoints[startIndex], ypoints[startIndex]); for (int j = startIndex + 1; j < length; j++) { path.lineTo(xpoints[j], ypoints[j]); } if (isPolygon) { path.closePath(); } } return path; } /** * Utility method that iterates over a Shape object and prints out the * points. */ public static void describeShapeDetail(Shape shape) { describeShapeDetail(shape, .25); } /** * Utility method that iterates over a Shape object and prints out the * points. The flattening is used for a FlatteningPathIterator, controlling * the scope of the path traversal. */ public static void describeShapeDetail(Shape shape, double flattening) { PathIterator pi2 = shape.getPathIterator(null); FlatteningPathIterator pi = new FlatteningPathIterator(pi2, flattening); double[] coords = new double[6]; int pointCount = 0; Debug.output(" -- start describeShapeDetail with flattening[" + flattening + "]"); while (!pi.isDone()) { int type = pi.currentSegment(coords); Debug.output(" Shape point [" + type + "] (" + (pointCount++) + ") " + coords[0] + ", " + coords[1]); pi.next(); } Debug.output(" -- end (" + pointCount + ")"); } /** * Convenience method to add the coordinates to the given GeneralPath. You * need to close the path yourself if you want it to be a polygon. * * @param toShape the GeneralPath Shape object to add the coordinates to. * @param xpoints horizontal pixel coordinates. * @param ypoints vertical pixel coordinates. * @return toShape, with coordinates appended. */ public static GeneralPath appendShapeEdge(GeneralPath toShape, float[] xpoints, float[] ypoints) { return appendShapeEdge(toShape, xpoints, ypoints, 0, xpoints.length); } /** * Convenience method to add the coordinates to the given GeneralPath. You * need to close the path yourself if you want it to be a polygon. * * @param toShape the GeneralPath Shape object to add the coordinates to. * @param xpoints horizontal pixel coordinates. * @param ypoints vertical pixel coordinates. * @param startIndex the index into pixel coordinate array to start reading * from. * @param length the number of coordinates to add. * @return toShape, with coordinates appended. */ public static GeneralPath appendShapeEdge(GeneralPath toShape, float[] xpoints, float[] ypoints, int startIndex, int length) { return appendShapeEdge(toShape, createShape(xpoints, ypoints, startIndex, length, false)); } /** * Convenience method to append the edge of a GeneralPath Shape to another * GeneralPath Shape. A PathIterator is used to figure out the points to use * to add to the toShape. You need to close the path yourself if you want it * to be a polygon. Assumes that the two paths should be connected. * * @param toShape the GeneralPath Shape object to add the edge to. * @param addShape the GeneralPath Shape to add to the toShape. * @return toShape, with coordinates appended. Returns addShape if toShape * was null. */ public static GeneralPath appendShapeEdge(GeneralPath toShape, GeneralPath addShape) { return appendShapeEdge(toShape, addShape, true); } /** * Convenience method to append the edge of a GeneralPath Shape to another * GeneralPath Shape. A PathIterator is used to figure out the points to use * to add to the toShape. You need to close the path yourself if you want it * to be a polygon. * * @param toShape the GeneralPath Shape object to add the edge to. * @param addShape the GeneralPath Shape to add to the toShape. * @param lineTo specify whether the first point of the appended path is * connected to the original path. True to connect. * @return toShape, with coordinates appended. Returns addShape if toShape * was null. */ public static GeneralPath appendShapeEdge(GeneralPath toShape, GeneralPath addShape, boolean lineTo) { boolean DEBUG = Debug.debugging("arealist"); int pointCount = 0; // If both null, return null. if (addShape == null) { return toShape; } if (toShape == null) { return addShape; } toShape.append(addShape, lineTo); return toShape; /* * * PathIterator pi2 = addShape.getPathIterator(null); * FlatteningPathIterator pi = new FlatteningPathIterator(pi2, .25); * double[] coords = new double[6]; * * while (!pi.isDone()) { int type = pi.currentSegment(coords); if * (lineTo) { if (DEBUG) { Debug.output(" adding point [" + type + "] (" * + (pointCount++) + ") " + (float) coords[0] + ", " + (float) * coords[1]); } toShape.lineTo((float) coords[0], (float) coords[1]); * * } else { if (DEBUG) { Debug.output("Creating new shape, first point " * + (float) coords[0] + ", " + (float) coords[1]); } * toShape.moveTo((float) coords[0], (float) coords[1]); lineTo = true; * } pi.next(); } * * if (DEBUG) { Debug.output(" -- end point (" + pointCount + ")"); } * * return toShape; */ } /** * Create a general path from a point plus a height and width; */ public static GeneralPath createBoxShape(float x, float y, int width, int height) { float[] xs = new float[4]; float[] ys = new float[4]; xs[0] = x; ys[0] = y; xs[1] = x + width; ys[1] = y; xs[2] = x + width; ys[2] = y + height; xs[3] = x; ys[3] = y + height; return createShape(xs, ys, true); } public void restore(OMGeometry source) { this.lineType = source.getLineType(); this.visible = source.isVisible(); this.attributes = source.getAttributes(); this.needToRegenerate = true; } }