// ********************************************************************** // // <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/MapBean.java,v $ // $RCSfile: MapBean.java,v $ // $Revision: 1.23 $ // $Date: 2009/02/05 18:46:11 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Paint; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.ContainerEvent; import java.awt.event.ContainerListener; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JComponent; import javax.swing.OverlayLayout; import com.bbn.openmap.event.CenterEvent; import com.bbn.openmap.event.CenterListener; import com.bbn.openmap.event.LayerEvent; import com.bbn.openmap.event.LayerListener; import com.bbn.openmap.event.PaintListener; import com.bbn.openmap.event.PaintListenerSupport; import com.bbn.openmap.event.PanEvent; import com.bbn.openmap.event.PanListener; import com.bbn.openmap.event.ProjectionChangeVetoException; import com.bbn.openmap.event.ProjectionEvent; import com.bbn.openmap.event.ProjectionListener; import com.bbn.openmap.event.ProjectionSupport; import com.bbn.openmap.event.ZoomEvent; import com.bbn.openmap.event.ZoomListener; import com.bbn.openmap.geo.Geo; import com.bbn.openmap.proj.Mercator; import com.bbn.openmap.proj.Proj; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.proj.ProjectionFactory; import com.bbn.openmap.proj.coords.LatLonPoint; import com.bbn.openmap.util.Debug; /** * The MapBean is the main component of the OpenMap Development Kit. It is a * Java Bean that manages and displays a map. A map is comprised of a projection * and a list of layers, and this class has methods that allow you to control * the projection parameters and to add and remove layers. Layers that are part * of the map receive dynamic notifications of changes to the underlying view * and projection. * <p> * Most of the methods in the MapBean are called from the Java AWT and Swing * code. These methods make the MapBean a good "Swing citizen" to its parent * components, and you should not need to invoke them. In general there are only * two reasons to call MapBean methods: controlling the projection, and adding * or removing layers. * <p> * When controlling the MapBean projection, simply call the method that applies * - setCenter, pan, zoom, etc. NOTE: If you are setting more than one parameter * of the projection, it's more efficient to getProjection(), directly set the * parameters of the projection object, and then call setProjection() with the * modified projection. That way, each ProjectionListener of the MapBean (each * layer) will only receive one projectionChanged() method call, as opposed to * receiving one for each projection adjustment. * <p> * To add or remove layers, use the add() and remove() methods that the MapBean * inherits from java.awt.Container. The add() method can be called with an * integer that indicates its desired position in the layer list. * <P> * Changing the default clipping area may cause some Layers to not be drawn * completely, depending on what the clipping area is set to and when the layer * is trying to get itself painted. When manually adjusting clipping area, make * sure that when restricted clipping is over that a full repaint occurs if * there is a chance that another layer may be trying to paint itself. * <P> * PropertyChangeListeners and ProjectionListeners both receive notifications of * the projection changes, but the PropertyChangeListeners receive them first. * If you want to have a component that limits the MapBean's projection * parameters, it should be a PropertyChangeListener on the MapBean, and throw a * ProjectionChangeVetoException whenever a Projection setting falls outside of * the limits. The ProjectionChangeVetoException should hold the alternate * settings allowed by the listener. When a ProjectionChangeVetoException is * thrown, all of the PropertyChangeListeners will receive another * PropertyChangeEvent notification, under the MapBean.projectionVetoed property * name. The old value for that property will be the rejected Projection object, * and the new value will be the ProjectionChangeVetoException containing the * new suggestions. The MapBean will then apply the suggestions and launch * another round of projection change notifications. The ProjectionListeners * only receive notification of Projections that have passed through the * PropertyChangeListeners. * * @see Layer */ public class MapBean extends JComponent implements ComponentListener, ContainerListener, ProjectionListener, PanListener, ZoomListener, LayerListener, CenterListener, SoloMapComponent { private static Logger logger = Logger.getLogger(MapBean.class.getName()); public static final String LayersProperty = "MapBean.layers"; public static final String CursorProperty = "MapBean.cursor"; public static final String BackgroundProperty = "MapBean.background"; public static final String ProjectionProperty = "MapBean.projection"; public static final String ProjectionVetoedProperty = "MapBean.projectionVetoed"; /** * OpenMap title. */ public static final String title = "OpenMap(tm)"; /** * OpenMap version. */ public static final String version = "5.1.14"; /** * Suppress the copyright message on initialization. */ public static boolean suppressCopyright = false; private static boolean DEBUG_TIMESTAMP = false; private static boolean DEBUG_THREAD = true; private static final String copyrightNotice = "OpenMap(tm) Version " + version + "\r\n" + " Copyright (C) BBNT Solutions LLC. All rights reserved.\r\n" + " See http://openmap-java.org/ for details.\r\n"; public final static float DEFAULT_CENTER_LAT = 0.0f; public final static float DEFAULT_CENTER_LON = 0.0f; // zoomed all the way out public final static float DEFAULT_SCALE = Float.MAX_VALUE; public final static int DEFAULT_WIDTH = 640; public final static int DEFAULT_HEIGHT = 480; protected int minHeight = 100; protected int minWidth = 100; protected Proj projection = new Mercator(new LatLonPoint.Double(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON), DEFAULT_SCALE, DEFAULT_WIDTH, DEFAULT_HEIGHT); protected ProjectionSupport projectionSupport; /** * Layers that are removed from the MapBean are held until the next * projection change. When the projection changes, they are notified that * they have been removed from the map. This list is kept so that toggling a * layer on and off won't cause them to get rid of their resources, in case * the user is just creating different views of the map. */ protected Vector<Layer> removedLayers = new Vector<Layer>(0); /** * Some users may want the layers deleted immediately when they are removed * from the map. This flag controls that. The default behavior is to hold a * reference to a layer and actually release it when the projection changes * (default = true). Set to false if you want the MapBean to tell a Layer it * has been removed immediately when it happens. */ protected boolean layerRemovalDelayed = true; /** * This vector is to let the layers know when they have been added to the * map. */ protected Vector<Layer> addedLayers = new Vector<Layer>(0); /** * The PaintListeners want to know when the map has been repainted. */ protected PaintListenerSupport painters = null; /** * The background color for this particular MapBean. If null, the setting * for the projection, which in turn is set in the Environment class, will * be used. */ protected Paint background = null; /** * The MapBeanRepaintPolicy to use to handler/filter/pace layer repaint() * requests. If not set, a StandardMapBeanRepaintPolicy will be used, which * forwards repaint requests to Swing normally. */ protected MapBeanRepaintPolicy repaintPolicy = null; /** * The angle, in radians, to rotate the map. 0.0 is north-up, clockwise is * positive. */ protected double rotationAngle = 0; public final static Color DEFAULT_BACKGROUND_COLOR = new Color(191, 239, 255); /** * Return the OpenMap Copyright message. * * @return String Copyright */ public static String getCopyrightMessage() { return copyrightNotice; } /** * Construct a MapBean. */ public MapBean() { this(true); } public MapBean(boolean useThreadedNotification) { if (logger.isLoggable(Level.FINE)) { debugmsg("MapBean()"); } if (!suppressCopyright) { Debug.output(copyrightNotice); } background = DEFAULT_BACKGROUND_COLOR; // Don't need one for every MapBean, just the first one. suppressCopyright = true; super.setLayout(new OverlayLayout(this)); projectionSupport = new ProjectionSupport(this, useThreadedNotification); addComponentListener(this); addContainerListener(this); // ---------------------------------------- // In a builder tool it seems that the OverlayLayout // makes the MapBean fail to resize. And since it has // no children by default, it has no size. So I add // a null Layer here to give it a default size. // ---------------------------------------- if (java.beans.Beans.isDesignTime()) { add(new Layer() { public void projectionChanged(ProjectionEvent e) { } public Dimension getPreferredSize() { return new Dimension(100, 100); } }); } setPreferredSize(new Dimension(projection.getWidth(), projection.getHeight())); DEBUG_TIMESTAMP = logger.isLoggable(Level.FINER); DEBUG_THREAD = logger.isLoggable(Level.FINER); } /** * Return a string-ified representation of the MapBean. * * @return String representing mapbean. */ public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } /** * Call when getting rid of the MapBean, it releases pointers to all * listeners and kills the ProjectionSupport thread. */ public void dispose() { setLayerRemovalDelayed(false); if (projectionSupport != null) { projectionSupport.dispose(); projectionSupport = null; } if (painters != null) { painters.clear(); painters = null; } if (addedLayers != null) { addedLayers.removeAllElements(); addedLayers = null; } currentLayers = null; projectionFactory = null; removeComponentListener(this); removeContainerListener(this); removeAll(); purgeAndNotifyRemovedLayers(); } /*---------------------------------------------------------------------- * Window System overrides *----------------------------------------------------------------------*/ /** * Adds additional constraints on possible children components. The new * component must be a Layer. This method included as a good container * citizen, and should not be called directly. Use the add() methods * inherited from java.awt.Container instead. * * @param comp Component * @param constraints Object * @param index int location */ protected final void addImpl(Component comp, Object constraints, int index) { if (comp instanceof Layer) { super.addImpl(comp, constraints, index); } else { throw new IllegalArgumentException("only Layers can be added to a MapBean"); } } /** * Prevents changing the LayoutManager. Don't let anyone change the * LayoutManager! This is called by the parent component and should not be * called directly. */ public final void setLayout(LayoutManager mgr) { throw new IllegalArgumentException("cannot change layout of Map"); } /** * Return the minimum size of the MapBean window. Included here to be a good * citizen. */ public Dimension getMinimumSize() { return new Dimension(minWidth, minHeight); } /** * Set the minimum size of the MapBean window. Included here to be a good * citizen. */ public void setMinimumSize(Dimension dim) { minWidth = (int) dim.getWidth(); minHeight = (int) dim.getHeight(); } /** * Get the Insets of the MapBean. This returns 0-length Insets. * <p> * This makes sure that there will be no +x,+y offset when drawing graphics. * This is ok since any borders around the MapBean will get drawn afterwards * on top. * * @return Insets 0-length Insets */ public final Insets getInsets() { return insets; } private final transient static Insets insets = new Insets(0, 0, 0, 0); /*---------------------------------------------------------------------- * ComponentListener implementation *----------------------------------------------------------------------*/ /** * ComponentListener interface method. Should not be called directly. * Invoked when component has been resized, and kicks off a projection * change. * * @param e ComponentEvent */ public void componentResized(ComponentEvent e) { if (logger.isLoggable(Level.FINE)) { logger.fine("Size changed: " + getWidth() + " x " + getHeight()); } projection.setWidth(getWidth()); projection.setHeight(getHeight()); fireProjectionChanged(); } /** * ComponentListener interface method. Should not be called directly. * Invoked when component has been moved. * * @param e ComponentEvent */ public void componentMoved(ComponentEvent e) { } /** * ComponentListener interface method. Should not be called directly. * Invoked when component has been shown. * * @param e ComponentEvent */ public void componentShown(ComponentEvent e) { } /** * ComponentListener interface method. Should not be called directly. * Invoked when component has been hidden. * * @param e ComponentEvent */ public void componentHidden(ComponentEvent e) { } /*---------------------------------------------------------------------- * *----------------------------------------------------------------------*/ /** * Add a ProjectionListener to the MapBean. You do not need to call this * method to add layers as ProjectionListeners. This method is called for * the layer when it is added to the MapBean. Use this method for other * objects that you want to know about the MapBean's projection. * * @param l ProjectionListener */ public synchronized void addProjectionListener(ProjectionListener l) { projectionSupport.add(l); // Assume that it wants the current projection try { l.projectionChanged(new ProjectionEvent(this, getRotatedProjection())); } catch (Exception e) { if (logger.isLoggable(Level.FINER)) { logger.fine("ProjectionListener not handling projection well: " + l.getClass().getName() + " : " + e.getClass().getName() + " : " + e.getMessage()); e.printStackTrace(); } } } /** * Remove a ProjectionListener from the MapBean. You do not need to call * this method to remove layers that are ProjectionListeners. This method is * called for the layer when it is removed from the MapBean. Use this method * for other objects that you want to remove from receiving projection * events. * * @param l ProjectionListener */ public synchronized void removeProjectionListener(ProjectionListener l) { projectionSupport.remove(l); } /** * Called from within the MapBean when its projection listeners need to know * about a projection change. */ protected void fireProjectionChanged() { // This handles setting up the RotationHelper if it's needed. Projection proj = getRotatedProjection(); // Fire the property change, so the messages get cleared out. // Then, if any of the layers have a problem with their new // projection, their messages will be displayed. if (logger.isLoggable(Level.FINE)) { logger.fine("MapBean firing projection: " + proj); } try { firePropertyChange(ProjectionProperty, null, proj); } catch (ProjectionChangeVetoException pcve) { firePropertyChange(ProjectionVetoedProperty, proj, pcve); pcve.updateWithParameters(this); return; } // Mark the layers as dirty, as a group, before notifying them of a // projection change. They will mark themselves clean when they call // repaint. for (Component c : getComponents()) { Layer l = (Layer) c; if (l != null) { // Weird, I know, but I've seen c be null and throw an // exception here. l.setReadyToPaint(false); } } projectionSupport.fireProjectionChanged(proj); purgeAndNotifyRemovedLayers(); } /** * Clear the vector containing all of the removed layers, and let those * layers know they have been removed from the map. */ public void purgeAndNotifyRemovedLayers() { // Tell any layers that have been removed that they have // been removed ArrayList<Layer> rLayers = new ArrayList<Layer>(removedLayers); removedLayers.clear(); if (rLayers.isEmpty()) { return; } for (Layer layer : rLayers) { layer.removed(this); } // Shouldn't call this, but it's the only thing // that seems to make it work... // Seems to help gc'ing layers in a timely manner. if (Debug.debugging("helpgc")) { System.gc(); } } /*---------------------------------------------------------------------- * Properties *----------------------------------------------------------------------*/ /** * Gets the scale of the map. * * @return float the current scale of the map * @see Projection#getScale */ public float getScale() { return projection.getScale(); } /** * Sets the scale of the map. The Projection may silently disregard this * setting, setting it to a <strong>maxscale </strong> or <strong>minscale * </strong> value. * * @param newScale the new scale * @see Proj#setScale */ public void setScale(float newScale) { projection.setScale(newScale); fireProjectionChanged(); } /** * Gets the center of the map in the form of a LatLonPoint. * * @return the center point of the map * @see Projection#getCenter */ public Point2D getCenter() { return projection.getCenter(); } /** * Sets the center of the map. * * @param newCenter the center point of the map * @see Proj#setCenter(Point2D) */ public void setCenter(Point2D newCenter) { projection.setCenter(newCenter); fireProjectionChanged(); } /** * Sets the center of the map. * * @param lat the latitude of center point of the map in decimal degrees * @param lon the longitude of center point of the map in decimal degrees * @see Proj#setCenter(double, double) */ public void setCenter(double lat, double lon) { projection.setCenter(new Point2D.Double(lon, lat)); fireProjectionChanged(); } /** * Sets the center of the map. * * @param lat the latitude of center point of the map in decimal degrees * @param lon the longitude of center point of the map in decimal degrees * @see Proj#setCenter(double, double) */ public void setCenter(float lat, float lon) { setCenter((double) lat, (double) lon); } /** * Set the background color of the map. If the background for this MapBean * is not null, the background of the projection will be used. * * @param color java.awt.Color. */ public void setBackgroundColor(Color color) { setBackground(color); } public void setBackground(Color color) { super.setBackground(color); setBckgrnd((Paint) color); } /** * We override this to set the paint mode on the Graphics before the border * is painted, otherwise we get an XOR effect in the border. */ public void paintBorder(Graphics g) { g.setPaintMode(); super.paintBorder(g); } /** * Set the background of the map. If the background for this MapBean is not * null, the background of the projection will be used. * * @param paint java.awt.Paint. */ public void setBckgrnd(Paint paint) { setBufferDirty(true); // Instead, do this. Paint oldBackground = background; background = paint; firePropertyChange(BackgroundProperty, oldBackground, background); repaint(); } /** * Get the background color of the map. If the background color for this * MapBean has been explicitly set, that value will be returned. Otherwise, * the background color of the projection will be returned. If the * background is not a color (as opposed to Paint) this method will return * null. * * @return color java.awt.Color. */ public Color getBackground() { Paint ret = getBckgrnd(); if (ret instanceof Color) { return (Color) ret; } return super.getBackground(); } /** * Get the background of the map. If the background for this MapBean has * been explicitly set, that value will be returned. Otherwise, the * background of the projection will be returned. * * @return color java.awt.Color. */ public Paint getBckgrnd() { Paint ret = background; if (ret == null) { ret = super.getBackground(); } return ret; } /** * Get the projection property, reflects the projection with no rotation. * * @return current Projection of map. */ public Projection getProjection() { return projection; } /** * @return the expanded rotated projection if map rotated, normal projection * if not rotated. The rotated projection is larger than the MapBean * and has extra offsets. */ public Projection getRotatedProjection() { RotationHelper rotation = getUpdatedRotHelper(); Projection proj = rotation != null ? rotation.getProjection() : projection; // Double check ((Proj) proj).setRotationAngle(getRotationAngle()); return proj; } /** * Set the projection. Shouldn't be null, and won't do anything if it is. * * @param aProjection Projection */ public void setProjection(Projection aProjection) { if (aProjection != null && !aProjection.getProjectionID().contains("NaN")) { setBufferDirty(true); projection = (Proj) aProjection; setPreferredSize(new Dimension(projection.getWidth(), projection.getHeight())); fireProjectionChanged(); } } // ------------------------------------------------------------ // CenterListener interface // ------------------------------------------------------------ /** * Handles incoming <code>CenterEvents</code>. * * @param evt the incoming center event */ public void center(CenterEvent evt) { setCenter(evt.getLatitude(), evt.getLongitude()); } // ------------------------------------------------------------ // PanListener interface // ------------------------------------------------------------ /** * Handles incoming <code>PanEvents</code>. * * @param evt the incoming pan event */ public void pan(PanEvent evt) { if (logger.isLoggable(Level.FINE)) { debugmsg("PanEvent: " + evt); } float az = evt.getAzimuth() - (float) Math.toDegrees(rotationAngle); float c = evt.getArcDistance(); if (Float.isNaN(c)) { projection.pan(az); } else { projection.pan(az, c); } fireProjectionChanged(); } // ------------------------------------------------------------ // ZoomListener interface // ------------------------------------------------------------ /** * Zoom the Map. Part of the ZoomListener interface. Sets the scale of the * MapBean projection, based on a relative or absolute amount. * * @param evt the ZoomEvent describing the new scale. */ public void zoom(ZoomEvent evt) { float newScale; if (evt.isAbsolute()) { newScale = evt.getAmount(); } else if (evt.isRelative()) { newScale = getScale() * evt.getAmount(); } else { return; } setScale(newScale); } // ------------------------------------------------------------ // ContainerListener interface // ------------------------------------------------------------ protected transient Layer[] currentLayers = new Layer[0]; protected transient boolean doContainerChange = true; /** * ContainerListener Interface method. Should not be called directly. Part * of the ContainerListener interface, and it's here to make the MapBean a * good Container citizen. * * @param value boolean */ public void setDoContainerChange(boolean value) { // if changing from false to true, call changeLayers() if (!doContainerChange && value) { doContainerChange = value; changeLayers(null); } else { doContainerChange = value; } } /** * ContainerListener Interface method. Should not be called directly. Part * of the ContainerListener interface, and it's here to make the MapBean a * good Container citizen. * * @return boolean */ public boolean getDoContainerChange() { return doContainerChange; } /** * ContainerListener Interface method. Should not be called directly. Part * of the ContainerListener interface, and it's here to make the MapBean a * good Container citizen. * * @param e ContainerEvent */ public void componentAdded(ContainerEvent e) { // Blindly cast. addImpl has already checked to be // sure the child is a Layer. Layer childLayer = (Layer) e.getChild(); addProjectionListener(childLayer); // If the new layer is in the queue to have removed() called // on it take it off the queue, and don't add it to the // added() queue (it doesn't know that it was removed, yet). // Otherwise, add it to the queue to have added() called on // it. if (!removedLayers.removeElement(childLayer)) { addedLayers.addElement(childLayer); } changeLayers(e); } /** * ContainerListener Interface method. Should not be called directly. Part * of the ContainerListener interface, and it's here to make the MapBean a * good Container citizen. Layers that are removed are added to a list, * which is cleared when the projection changes. If they are added to the * MapBean again before the projection changes, they are taken off the list, * added back to the MapBean, and are simply repainted. This prevents layers * from doing unnecessary work if they are toggled on and off without * projection changes. * * @param e ContainerEvent * @see com.bbn.openmap.MapBean#purgeAndNotifyRemovedLayers */ public void componentRemoved(ContainerEvent e) { // Blindly cast. addImpl has already checked to be // sure the child is a Layer. Layer childLayer = (Layer) e.getChild(); removeProjectionListener(childLayer); removedLayers.addElement(childLayer); changeLayers(e); } /** * ContainerListener Interface method. Should not be called directly. Part * of the ContainerListener interface, and it's here to make the MapBean a * good Container citizen. * * @param e ContainerEvent */ protected void changeLayers(ContainerEvent e) { // Container Changes can be disabled to speed adding/removing // multiple layers if (!doContainerChange) { return; } Component[] comps = this.getComponents(); int ncomponents = comps.length; Layer[] newLayers = new Layer[ncomponents]; System.arraycopy(comps, 0, newLayers, 0, ncomponents); if (logger.isLoggable(Level.FINE)) { debugmsg("changeLayers() - firing change"); } firePropertyChange(LayersProperty, currentLayers, newLayers); // Tell the new layers that they have been added for (Layer layer : addedLayers) { layer.added(this); } addedLayers.removeAllElements(); currentLayers = newLayers; } // ------------------------------------------------------------ // ProjectionListener interface // ------------------------------------------------------------ /** * ProjectionListener interface method. Should not be called directly. * * @param e ProjectionEvent */ public void projectionChanged(ProjectionEvent e) { Projection newProj = e.getProjection(); if (!projection.equals(newProj)) { setProjection(newProj); } } /** * Set the Mouse cursor over the MapBean component. * * @param newCursor Cursor */ public void setCursor(Cursor newCursor) { firePropertyChange(CursorProperty, this.getCursor(), newCursor); super.setCursor(newCursor); } /** * In addition to adding the PropertyChangeListener as the JComponent method * does, this method also provides the listener with the initial version of * the Layer and Cursor properties. */ public void addPropertyChangeListener(PropertyChangeListener pcl) { super.addPropertyChangeListener(pcl); pcl.propertyChange(new PropertyChangeEvent(this, LayersProperty, currentLayers, currentLayers)); pcl.propertyChange(new PropertyChangeEvent(this, CursorProperty, this.getCursor(), this.getCursor())); pcl.propertyChange(new PropertyChangeEvent(this, BackgroundProperty, this.getBckgrnd(), this.getBckgrnd())); } protected final void debugmsg(String msg) { logger.fine(this.toString() + (DEBUG_TIMESTAMP ? (" [" + System.currentTimeMillis() + "]") : "") + (DEBUG_THREAD ? (" [" + Thread.currentThread() + "]") : "") + ": " + msg); } /** * Same as JComponent.paint(), except if there are no children (Layers), the * projection still paints the background and the border is painted. */ public void paint(Graphics g) { if (projection != null) { drawProjectionBackground(g); } if (this.getComponentCount() > 0) { paintChildren(g, null); } paintPainters(g); // Border gets painted over by printChildren with special layer // handling. paintBorder(g); } /** * Convenience method to test if Graphics is Graphics2D object, and to try * to do the right thing. */ protected void drawProjectionBackground(Graphics g) { if (g instanceof Graphics2D) { projection.drawBackground((Graphics2D) g, getBckgrnd()); } else { g.setColor(getBackground()); projection.drawBackground(g); } } /** * Same as JComponent.paintChildren() except any PaintListeners are notified * and the border is painted over the children. */ public void paintChildren(Graphics g) { paintChildren(g, null); paintPainters(g); } public void paintPainters(Graphics g) { // Just want a quick, non-changing handle on the helper. Don't need to // configure it. RotationHelper rotationHelper = getRotHelper(); if (painters != null) { if (rotationHelper != null) { rotationHelper.paintPainters(g); } else { painters.paint(g); } } } /** * Same as paintChildren, but allows you to set a clipping area to paint. Be * careful with this, because if the clipping area is set while some layer * decides to paint itself, that layer may not have all it's objects * painted. */ public void paintChildren(Graphics g, Rectangle clip) { g = getMapBeanRepaintPolicy().modifyGraphicsForPainting(g); drawProjectionBackground(g); RotationHelper rotationHelper = getRotHelper(); if (rotationHelper != null) { rotationHelper.paintChildren(g, clip); } else { // Normal painting super.paintChildren(g); } } /** * A method that grabs the component list of the MapBean, and renders just * the layers from back to front. No clipping is set, other than what is set * on the Graphics object. * * @param g Graphics */ protected void paintLayers(Graphics g) { synchronized (getTreeLock()) { int i = getComponentCount() - 1; if (i < 0) { return; } for (; i >= 0; i--) { Component comp = getComponent(i); final boolean isLayer = comp instanceof Layer; if (isLayer && comp.isVisible()) { comp.paint(g); } } } } public Graphics getGraphics(boolean rotateIfSet) { RotationHelper rotationHelper = getRotHelper(); if (rotateIfSet && rotationHelper != null) { return rotationHelper.getGraphics(); } return super.getGraphics(); } /** * Method that provides an option of whether or not to draw the border when * painting. Usually called from another object trying to control the Map * appearance when events are flying around. */ public void paintChildrenWithBorder(Graphics g, boolean drawBorder) { paintChildren(g); if (drawBorder) { paintBorder(g); } } /** * Add a PaintListener. * * @param l PaintListener */ public synchronized void addPaintListener(PaintListener l) { if (painters == null) { painters = new PaintListenerSupport(this); } painters.add(l); } /** * Remove a PaintListener. * * @param l PaintListener */ public synchronized void removePaintListener(PaintListener l) { if (painters == null) { return; } painters.remove(l); // Should we get rid of the support if there are no painters? // The support will get created when a listener is added. if (painters.isEmpty()) { painters = null; } } // ------------------------------------------------------------ // LayerListener interface // ------------------------------------------------------------ /** * LayerListener interface method. A list of layers will be added, removed, * or replaced based on on the type of LayerEvent. * * @param evt a LayerEvent */ public void setLayers(LayerEvent evt) { setBufferDirty(true); Layer[] layers = evt.getLayers(); int type = evt.getType(); if (type == LayerEvent.ALL) { // Don't care about these at all... return; } // @HACK is this cool?: if (layers == null) { if (logger.isLoggable(Level.FINE)) { debugmsg("MapBean.setLayers(): layers is null!"); } return; } boolean oldChange = getDoContainerChange(); setDoContainerChange(false); // use LayerEvent.REPLACE when you want to remove all current // layers add a new set if (type == LayerEvent.REPLACE) { if (logger.isLoggable(Level.FINE)) { debugmsg("Replacing all layers"); } removeAll(); for (Layer layer : layers) { if (layer == null) { if (logger.isLoggable(Level.FINE)) { debugmsg("MapBean.setLayers(): skipping null layer from being added to MapBean"); } continue; } if (logger.isLoggable(Level.FINE)) { debugmsg("Adding layer[" + layer.getName() + "]"); } add(layer); layer.setVisible(true); } } // use LayerEvent.ADD when adding and/or reshuffling layers else if (type == LayerEvent.ADD) { if (logger.isLoggable(Level.FINE)) { debugmsg("Adding new layers"); } for (Layer layer : layers) { if (logger.isLoggable(Level.FINE)) { debugmsg("Adding layer[" + layer.getName() + "]"); } add(layer); layer.setVisible(true); } } // use LayerEvent.REMOVE when you want to delete layers from // the map else if (type == LayerEvent.REMOVE) { if (logger.isLoggable(Level.FINE)) { debugmsg("Removing layers"); } for (Layer layer : layers) { if (logger.isLoggable(Level.FINE)) { debugmsg("Removing layer[" + layer.getName() + "]"); } remove(layer); } } if (!layerRemovalDelayed) { purgeAndNotifyRemovedLayers(); } setDoContainerChange(oldChange); revalidate(); repaint(); } /** * A call to try and get the MapBean to reduce flashing by controlling when * repaints happen, waiting for lower layers to call for a repaint(), too. * Calls shouldForwardRepaint(Layer), which acts as a policy for whether to * forward the repaint up the Swing tree. */ public void repaint(Layer layer) { setBufferDirty(true); if (logger.isLoggable(Level.FINER)) { String name = layer.getName(); logger.finer((name == null ? layer.getClass().getName() : name) + " - wants a repaint()"); } getMapBeanRepaintPolicy().repaint(layer); } /** * Set the MapBeanRepaintPolicy used by the MapBean. This policy can be used * to pace/filter layer repaint() requests. */ public void setMapBeanRepaintPolicy(MapBeanRepaintPolicy mbrp) { repaintPolicy = mbrp; } /** * Get the MapBeanRepaintPolicy used by the MapBean. This policy can be used * to pace/filter layer repaint() requests. If no policy has been set, a * StandardMapBeanRepaintPolicy will be created, which simply forwards all * requests. */ public MapBeanRepaintPolicy getMapBeanRepaintPolicy() { if (repaintPolicy == null) { repaintPolicy = new StandardMapBeanRepaintPolicy(this); } return repaintPolicy; } /** * Convenience function to get the LatLonPoint representing a screen * location from a MouseEvent. Returns null if the event is null, or if the * projection is not set in the MapBean. Allocates new LatLonPoint with * coordinates. Takes rotation set on MapBean into account. */ public Point2D getCoordinates(MouseEvent event) { return getCoordinates(event, null); } /** * Convenience function to get the LatLonPoint representing a screen * location from a MouseEvent. Returns null if the event is null, or if the * projection is not set in the MapBean. Save on memory allocation by * sending in the LatLonPoint to fill. Takes rotation set on MapBean into * account. */ public <T extends Point2D> T getCoordinates(MouseEvent event, T llp) { Projection proj = getProjection(); if (proj == null || event == null) { return null; } return inverse(event.getX(), event.getY(), llp); } /** * Convenience function to get the pixel Point2D representing a screen * location from a MouseEvent in the projection space (as if there is no * rotation set). Returns null if the event is null. This is used to talk to * the OMGraphics, since they don't know about the map rotation. */ public Point2D getNonRotatedLocation(MouseEvent event) { return getNonRotatedLocation(event, null); } /** * Convenience function to get the pixel Point2D representing a screen * location from a MouseEvent in the projection space (as if there is no * rotation set). Returns null if the event is null. This is used to talk to * the OMGraphics, since they don't know about the map rotation. */ public Point2D getNonRotatedLocation(MouseEvent event, Point2D pnt) { if (event == null) { return null; } if (pnt == null) { pnt = new Point2D.Double(event.getX(), event.getY()); } else { pnt.setLocation(event.getX(), event.getY()); } RotationHelper rotationHelper = getRotHelper(); if (rotationHelper != null) { pnt = rotationHelper.inverseTransform(pnt, pnt); } return pnt; } /** * If the map has been rotated, get a shape that has been transformed into * the pixel space of the unrotated maps (the space the projected OMGraphics * know about). * * @param shape input shape * @return GeneralPath for transform shape if map is rotated, the input * shape if the map is not rotated. */ public Shape getNonRotatedShape(Shape shape) { RotationHelper rotationHelper = getRotHelper(); if (rotationHelper != null) { return rotationHelper.inverseTransform(shape); } return shape; } /** * Checks the rotation set on the MapBean and accounts for it before calling * inverse on the projection. * * @param x horizontal window pixel from left side * @param y vertical window pixel from top * @param ret Point2D object returned with coordinates suitable for * projection where mouse event is. * @return the provided T ret object, or new Point2D object from projection * if ret is null. */ public <T extends Point2D> T inverse(double x, double y, T ret) { RotationHelper rotationHelper = getRotHelper(); return (rotationHelper == null) ? getProjection().inverse(x, y, ret) : rotationHelper.inverse(x, y, ret); } /** * Interface-like method to query if the MapBean is buffered, so you can * control behavior better. Allows the removal of specific instance-like * queries for, say, BufferedMapBean, when all you really want to know is if * you have the data is buffered, and if so, should be buffer be cleared. * For the MapBean, always false. */ public boolean isBuffered() { return false; } /** * Interface-like method to set a buffer dirty, if there is one. In MapBean, * there isn't. * * @param value boolean */ public void setBufferDirty(boolean value) { } /** * Checks whether the image buffer should be repainted. * * @return boolean whether the layer buffer is dirty. Always true for * MapBean, because a paint is always gonna need to happen. */ public boolean isBufferDirty() { return true; } /** * If true (default) layers are held when they are removed, and then * released and notified of removal when the projection changes. This saves * the layers from releasing resources if the layer is simply being toggled * on/off for different map views. * * @param set the setting */ public void setLayerRemovalDelayed(boolean set) { layerRemovalDelayed = set; } /** * @return the flag for delayed layer removal. */ public boolean isLayerRemovalDelayed() { return layerRemovalDelayed; } /** * Go through the layers, and for all of them that have the autoPalette * variable turned on, show their palettes. */ public void showLayerPalettes() { for (Component comp : getComponents()) { // they have to be layers Layer l = (Layer) comp; if (l.autoPalette) { l.showPalette(); } } } /** * Turn off all layer palettes. */ public void hideLayerPalettes() { for (Component comp : getComponents()) { // they have to be layers ((Layer) comp).hidePalette(); } } protected ProjectionFactory projectionFactory; public ProjectionFactory getProjectionFactory() { if (projectionFactory == null) { projectionFactory = ProjectionFactory.loadDefaultProjections(); } return projectionFactory; } public void setProjectionFactory(ProjectionFactory projFactory) { projectionFactory = projFactory; } protected RotationHelper rotHelper; /** * Handles all of the updating of the RotationHelper if needed, based on the * current rotation settings on the MapBean. * * @return the locRotHelper, null if not needed. */ protected RotationHelper getUpdatedRotHelper() { double rotAngle = getRotationAngle(); Projection proj = getProjection(); RotationHelper rotationHelper = getRotHelper(); if (rotAngle != 0.0) { if (rotationHelper == null) { rotationHelper = new RotationHelper(rotAngle, proj); setRotHelper(rotationHelper); } else { rotationHelper.updateForBufferDimensions(proj); rotationHelper.updateAngle(rotAngle); } } else if (rotationHelper != null) { /* * Just because the angle is zero, let's check with the * rotationHelper. If the map is just passing through zero rotation, * keep it around. If we get a couple of projection changes with the * az set to zero, then get rid of the rotation helper. */ if (rotationHelper.isStillNeeded(rotAngle)) { rotationHelper.updateForBufferDimensions(proj); rotationHelper.updateAngle(rotAngle); } else { setRotHelper(null); rotationHelper = null; } } // else return null rotationHelper return rotationHelper; } /** * Get the RotationHelper that assists with rotated maps. * * @return RotationHelper, may be null if map isn't rotated. */ protected RotationHelper getRotHelper() { return rotHelper; } /** * @param nRotHelper the locRotHelper to set as the current one. Disposes of * the old one. */ protected void setRotHelper(RotationHelper nRotHelper) { RotationHelper rotationHelper = this.rotHelper; if (rotationHelper != null) { rotationHelper.dispose(); } this.rotHelper = nRotHelper; } /** * Set the rotation of the map in RADIANS. * * @param angle radians of rotation, increasing clockwise. */ public void setRotationAngle(double angle) { setRotationAngle(angle, false); } /** * Set the rotation of the map in RADIANS. * * @param angle radians of rotation, increasing clockwise. * @param fastRotation if true, fireProjectionChange will not be called, and * the RotationHelper will be used to spin image buffer. */ public void setRotationAngle(double angle, boolean fastRotation) { if (this.rotationAngle != angle) { this.rotationAngle = angle; /* * moving into this block makes rotation work faster, and smooth. * However, it doesn't give the non-rotating OMGraphics a chance to * counteract the rotation. */ if (fastRotation && angle != 0) { /* * If only the angle changes, we can just update the * locRotHelper angle, and reuse all of the other settings. If * the angle changes and zero is involved,either way, get the * rotation helper set up in fireProjectionChanged. The * RotationHelper needs to be redefined for any other projection * changes anyway. */ RotationHelper locRotHelper = getRotHelper(); if (locRotHelper != null) { locRotHelper.updateAngle(angle); repaint(); return; } } fireProjectionChanged(); } } /** * Get the rotation of the map in RADIANS. * * @return the angle the map has been rotated, in RADIANS, clockwise is * positive. */ public double getRotationAngle() { return rotationAngle; } protected class RotationHelper { Image rotImage; double angle; Point2D rotCenter; int rotBufferHeight; int rotBufferWidth; int rotXOffset; int rotYOffset; Projection rotProjection; AffineTransform rotTransform; private RotationHelper(double angle, Projection currentProjection) { updateForBufferDimensions(currentProjection); updateAngle(angle); } /** * We're going to try to do buffering with a image that will cover all * of the corners when the map is rotated. We'll measure the ground * distance from the center of the projection/map to each corner, and * take the longest to create a bounding circle. The NSEW of that * bounding circle (as a bounding box) Makes up the buffered image pixel * bounds, and the inverse projected coordinates of that box should be * returned as upper left and lower right coordinates when those methods * are called. The projection of that box should be the same as the * current projection, except for the new width and height. * * Because the height and width are different for the buffered image, * we're going to have to translate it before it is rotated. We can * probably just tack on an additional translate to the rot. That * difference will be 1/2 the difference of the height and width between * the rot image and the original projection (mapbean dimensions). * * @param proj the projection to use to create the current image buffer * @return boolean true if the rotBufferHeight and/or rotBufferWidth * have changed, indicating that the image buffer was recreated * for new dimensions. */ protected boolean updateForBufferDimensions(Projection proj) { int currentRotBufferWidth = rotBufferWidth; int currentRotBufferHeight = rotBufferHeight; Point2D center = proj.getCenter(); Point2D ul = proj.getUpperLeft(); Point2D lr = proj.getLowerRight(); /* * Woooooow, we're really going to have to work it, aren't we? We * need to handle GeoProj differently than Cartesian coords. That * seems to lend itself to moving this kind of calculations to the * super classes of the projection classes. *sigh* * * For now, let's try assuming that GeoProj */ Geo centerGeo = new Geo(center.getY(), center.getX()); Geo ulGeo = new Geo(ul.getY(), ul.getX()); Geo lrGeo = new Geo(lr.getY(), lr.getX()); // Comparing the UL and LR corners for distance, get the greatest. double dist = Math.max(centerGeo.distance(ulGeo), centerGeo.distance(lrGeo)); // Now calculate the bounds of that distance in 4 directions Geo N = Geo.offset(centerGeo, dist, 0); Geo S = Geo.offset(centerGeo, dist, Math.PI); Geo E = Geo.offset(centerGeo, dist, Math.PI / 2.0); Geo W = Geo.offset(centerGeo, dist, -Math.PI / 2); // Calculate the coordinates of new bounds for that distance from // center. Point2D newUL = new Point2D.Double(W.getLongitude(), N.getLatitude()); Point2D newLR = new Point2D.Double(E.getLongitude(), S.getLatitude()); // Calculate the pixel bounds of the new bounding box to get new // projection h, w Point2D newULPix = proj.forward(newUL); Point2D newLRPix = proj.forward(newLR); int reqRotBufferHeight = (int) Math.abs(newLRPix.getY() - newULPix.getY()); int reqRotBufferWidth = (int) Math.abs(newLRPix.getX() - newULPix.getX()); // If the image is a little bigger than we need, we can reuse. Only // replace it if it is significantly bigger, or at all smaller. boolean needNewHeightImage = reqRotBufferHeight > currentRotBufferHeight || reqRotBufferHeight < .9 * currentRotBufferHeight; boolean needNewWidthImage = reqRotBufferWidth > currentRotBufferWidth || currentRotBufferWidth < .9 * currentRotBufferWidth; boolean bufferImageResized = false; if (needNewHeightImage || needNewWidthImage) { this.rotImage = new BufferedImage(reqRotBufferWidth, reqRotBufferHeight, BufferedImage.TYPE_INT_ARGB); rotBufferWidth = reqRotBufferWidth; rotBufferHeight = reqRotBufferHeight; bufferImageResized = true; } rotProjection = projectionFactory.makeProjection(proj.getClass(), center, proj.getScale(), rotBufferWidth, rotBufferHeight); this.rotCenter = rotProjection.forward(center); /* * Now calculate the different in size between the current * projection and the buffered image projection, and the offset * needed for translation for proper painting. */ this.rotXOffset = (rotProjection.getWidth() - proj.getWidth()) / 2; this.rotYOffset = (rotProjection.getHeight() - proj.getHeight()) / 2; return bufferImageResized; } public void updateAngle(double angle) { this.angle = angle; this.rotTransform = AffineTransform.getRotateInstance(angle, rotCenter.getX(), rotCenter.getY()); } /** * @param az angle to test against * @return true if current angle or new angle is not zero. Two zero * angles in a row is an indication that the RotationHelper is * no longer needed. */ public boolean isStillNeeded(double az) { return !(az == 0.0 && angle == 0.0); } /** * @return the projection of the image buffer that is big enough for * rotated areas. */ public Projection getProjection() { return rotProjection; } public void paintChildren(Graphics g, Rectangle clip) { if (rotProjection == null) { // We're not properly prepared for rotation, return; return; } Graphics2D g2 = (Graphics2D) rotImage.getGraphics(); ((Proj) rotProjection).drawBackground(g2, getBckgrnd()); g2.setTransform(rotTransform); paintLayers(g2); g.drawImage(rotImage, -rotXOffset, -rotYOffset, null); g2.dispose(); } public void paintPainters(Graphics g) { if (painters != null) { int x = getX(); int y = getY(); Graphics2D g2 = (Graphics2D) g.create(); AffineTransform transform = AffineTransform.getTranslateInstance(-rotXOffset + getX(), -rotYOffset + getY()); transform.concatenate(rotTransform); g2.setTransform(transform); painters.paint(g2); g2.dispose(); } } /** * @return a Graphics object from the MapBean with the rotation * transform applied. */ public Graphics getGraphics() { Graphics2D g = (Graphics2D) MapBean.super.getGraphics().create(); g.setTransform(rotTransform); return g; } /** * Performs a projection.inverse operation that also takes into account * rotation. * * @param x pixel x * @param y pixel y * @param ret T in the coordinate space of projection. * @return T, either ret or a new object. */ public <T extends Point2D> T inverse(double x, double y, T ret) { Point2D pnt = new Point2D.Double(x + rotXOffset, y + rotYOffset); try { pnt = rotTransform.inverseTransform(pnt, pnt); return getProjection().inverse(pnt, ret); } catch (NoninvertibleTransformException e) { logger.log(Level.FINE, e.getMessage(), e); } return ret; } /** * Returns dst, the unrotated pixel location of the map. * * @param src the pixel point * @param dst * @return see above. */ public Point2D inverseTransform(Point2D src, Point2D dst) { try { src.setLocation(src.getX() + rotXOffset, src.getY() + rotYOffset); dst = rotTransform.inverseTransform(src, dst); } catch (NoninvertibleTransformException e) { logger.log(Level.FINE, e.getMessage(), e); } return dst; } /** * Returns a transformed version of the Shape, unrotated into the * projected pixel space of the layer OMGraphics. * * @param shape to transform * @return the transformed shape. */ public Shape inverseTransform(Shape shape) { float[] coords = new float[6]; GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); PathIterator pi = shape.getPathIterator(getInverseRotationTransform()); while (!pi.isDone()) { int type = pi.currentSegment(coords); if (type == PathIterator.SEG_MOVETO) { path.moveTo(coords[0], coords[1]); } else if (type == PathIterator.SEG_LINETO) { path.lineTo(coords[0], coords[1]); } else if (type == PathIterator.SEG_CLOSE) { path.closePath(); } else { if (type == PathIterator.SEG_QUADTO) { path.quadTo(coords[0], coords[1], coords[2], coords[3]); } else if (type == PathIterator.SEG_CUBICTO) { path.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); } } pi.next(); } return path; } public AffineTransform getInverseRotationTransform() { try { AffineTransform translateOffset = AffineTransform.getTranslateInstance(rotXOffset, rotYOffset); AffineTransform transform = rotTransform.createInverse(); translateOffset.preConcatenate(transform); return translateOffset; } catch (NoninvertibleTransformException e) { logger.log(Level.FINE, "AffineTransform problem", e); } return new AffineTransform(); } public void dispose() { if (rotImage != null) { rotImage.flush(); } } } }