// ********************************************************************** // // <copyright> // // BBN Technologies // 10 Moulton Street // Cambridge, MA 02138 // (617) 873-8000 // // Copyright (C) BBNT Solutions LLC. All rights reserved. // // </copyright> // ********************************************************************** // // $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/BufferedLayer.java,v $ // $RCSfile: BufferedLayer.java,v $ // $Revision: 1.13 $ // $Date: 2008/10/01 15:26:30 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap.layer; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Paint; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ContainerEvent; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyVetoException; import java.beans.beancontext.BeanContext; import java.util.Properties; import java.util.Vector; import java.util.logging.Level; import javax.swing.JCheckBox; import javax.swing.JPanel; import javax.swing.JTabbedPane; import com.bbn.openmap.BufferedMapBean; import com.bbn.openmap.Layer; import com.bbn.openmap.LayerHandler; import com.bbn.openmap.MapBean; import com.bbn.openmap.event.ProjectionEvent; import com.bbn.openmap.omGraphics.OMColor; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.util.PropUtils; /** * A BufferedLayer is a layer that buffers a group of layers into an image. When * this layer repaints, the image gets rendered. This layer can be used to group * a set of layers into one, and was designed with the idea that it is a * background layer where a more animated layer would be on top of it. * <P> * * This layer contains a MapBean, and any layer that gets added to it simply * gets added to the MapBean. When a layer needs to redraw itself, it can act * normally, and the BufferedLayer will get updated as needed. If the MapBean is * a BufferedMapBean (which it is by default), then the layers will get buffered * into an image. * <P> * * The BufferedLayer can be configured in the openmap.properties file: * * <pre> * * * bufLayer.class=com.bbn.openmap.layer.BufferedLayer * bufLayer.prettyName=My Layer Group * bufLayer.layers=layer1 layer2 layer3 * bufLayer.visibleLayers=layer1 layer3 * </pre> * * layer1, layer2, etc should be defined as any other openmap layer. */ public class BufferedLayer extends OMGraphicHandlerLayer implements PropertyChangeListener { private static final long serialVersionUID = 1L; public final static String LayersProperty = "layers"; public final static String VisibleLayersProperty = "visibleLayers"; /** * Used to tell the BufferedLayer that the background is transparent. Will * cause a new image buffer to be created when the projection changes, in * order to cover up what was already there. This is set to true but * default, since the internal MapBean color is set to OMColor.clear. */ protected boolean hasTransparentBackground = true; /** * The MapBean used as the group organized. If this is a BufferedMapBean, * the layer will provide a buffered image. */ MapBean mapBean; public BufferedLayer() { this.setLayout(new BorderLayout()); // Adds the mapbean to the layer MapBean mb = new BLMapBean(); // Add it the layer properly... setMapBean(mb); } public void setProperties(String prefix, Properties props) { super.setProperties(prefix, props); prefix = PropUtils.getScopedPropertyPrefix(prefix); PropUtils.putDataPrefixToLayerList(this, props, prefix + LayersProperty); Vector<String> layersValue = PropUtils.parseSpacedMarkers(props.getProperty(prefix + LayersProperty)); Vector<String> startuplayers = PropUtils.parseSpacedMarkers(props.getProperty(prefix + VisibleLayersProperty)); Layer[] layers = LayerHandler.getLayers(layersValue, startuplayers, props); for (Layer layer : layers) { mapBean.add(layer); } } public Properties getProperties(Properties props) { props = super.getProperties(props); String prefix = PropUtils.getScopedPropertyPrefix(this); StringBuffer layersListProperty = new StringBuffer(); StringBuffer startupLayersListProperty = new StringBuffer(); Component[] comps = mapBean.getComponents(); for (int i = 0; i < comps.length; i++) { // they have to be layers Layer layer = (Layer) comps[i]; String lPrefix = layer.getPropertyPrefix(); boolean unsetPrefix = false; if (lPrefix == null) { lPrefix = "layer" + i; // I think we need to do this, in order to get proper // scoping in the properties. We'll unset it later... layer.setPropertyPrefix(lPrefix); unsetPrefix = true; } layersListProperty.append(" ").append(lPrefix); if (layer.isVisible()) { startupLayersListProperty.append(" ").append(lPrefix); } if (logger.isLoggable(Level.FINE)) { logger.fine("BufferedLayer: getting properties for " + layer.getName() + " " + layer.getProperties(new Properties())); } layer.getProperties(props); if (unsetPrefix) { layer.setPropertyPrefix(null); } } props.put(prefix + LayersProperty, layersListProperty.toString()); props.put(prefix + VisibleLayersProperty, startupLayersListProperty.toString()); return props; } /** * Not really implemented, because the mechanism for providing a set of * properties that let you add a variable number of new objects as children * to this one. */ public Properties getPropertyInfo(Properties props) { props = super.getPropertyInfo(props); return props; } /** * If true, will create a new image buffer when the projection changes. * Should be set to true if the background has any transparency. */ public void setHasTransparentBackground(boolean value) { hasTransparentBackground = value; } public boolean getHasTransparentBackground() { return hasTransparentBackground; } /** * Remove all layers from the group. */ public void clearLayers() { Component[] layers = getLayers(); if (layers != null && layers.length > 0) { for (int i = 0; i < layers.length; i++) { removeLayer((Layer) layers[i]); } } resetPalette(); } /** * Method for BeanContextChild interface. Gets an iterator from the * BeanContext to call findAndInit() over. Sets BeanContext on sub-layers. */ public void setBeanContext(BeanContext in_bc) throws PropertyVetoException { super.setBeanContext(in_bc); Component[] layers = getLayers(); if (layers != null && layers.length > 0) { for (int i = 0; i < layers.length; i++) { ((Layer) layers[i]).setBeanContext(in_bc); } } } /** * Add a layer to the group. Sets the BeanContext on the added layer. */ public void addLayer(Layer layer) { mapBean.add(layer); try { layer.setBeanContext(getBeanContext()); } catch (PropertyVetoException nve) { } resetPalette(); } /** * Remove the layer from group. */ public void removeLayer(Layer layer) { mapBean.remove(layer); resetPalette(); } /** * Return if there is at least one layer assigned to the group. */ public boolean hasLayers() { return (mapBean.getComponentCount() > 0); } /** * Get the layers assigned to the internal MapBean. * * @return a Component[]. */ public Component[] getLayers() { return mapBean.getComponents(); } /** * Returns true if all children are are ready to be painted - that is, they * have called for a repaint. * * @return the readyToPaint */ public boolean isReadyToPaint() { for (Component c : getLayers()) { if (!((Layer) c).isReadyToPaint()) { return false; } } return true; } /** * Set whether this layer should be ready to paint. Called when the * projection changes, to make child layers 'dirty'. We don't change the * super classes' version of the variable, because it's never consulted and * it's probably better that it's always true by default. * * @param readyToPaint the readyToPaint to set */ public void setReadyToPaint(boolean readyToPaint) { for (Component c : getLayers()) { ((Layer) c).setReadyToPaint(readyToPaint); } } /** * You can change what kind of MapBean is used to hold onto the layers. This * method just sets the new MapBean into the layer, as is. If there was a * previous MapBean with layers, they're gone and replaces with whatever is * attached to the new MapBean. * * @param mb new MapBean */ public void setMapBean(MapBean mb) { if (mapBean != null) { remove(mapBean); } mapBean = mb; add(mapBean, BorderLayout.CENTER); } /** * Get the current MapBean used in the BufferedLayer. * * @return MapBean */ public MapBean getMapBean() { return mapBean; } /** * Set the background color of the group. Actually sets the background color * of the projection used by the internal MapBean, and which then forces a * repaint() on it. * * @param color java.awt.Color. */ public void setBackground(Color color) { setBckgrnd(color); } /** * Set the background paint of the group. Actually sets the background paint * of the projection used by the internal MapBean. * * @param paint java.awt.Paint */ public void setBckgrnd(Paint paint) { mapBean.setBckgrnd(paint); if (paint instanceof Color) { setHasTransparentBackground(((Color) paint).getAlpha() < 255); } else { // then we don't know, assume it is. setHasTransparentBackground(true); } } /** * Get the background color of the image. Actually returns the background * color of the projection of the internal MapBean. * * @return color java.awt.Color */ public Color getBackground() { return mapBean.getBackground(); } /** * Get the background Paint object used for the internal MapBean. * * @return java.awt.Paint */ public Paint getBckgrnd(Paint paint) { return mapBean.getBckgrnd(); } /** * We don't want a projection change policy called here, because that will * call for a repaint. We want the buffered layers to call for a repaint if * necessary. */ @Override public void projectionChanged(ProjectionEvent pevent) { // Just pass it on, let the layers decide if they have to update. mapBean.setProjection(pevent.getProjection()); } /** * Called when the layer is removed from the MapBean, and after the * projection changes. Lets the layer know to release memory that might not * be needed. */ @Override public void removed(Container cont) { Component[] layers = getLayers(); if (layers != null && layers.length > 0) { for (int i = 0; i < layers.length; i++) { ((Layer) layers[i]).remove(cont); } } } /** * The GUI panel. */ JPanel panel = null; /** * Should be called if layers are added or removed from the buffer. */ public void resetPalette() { panel = null; super.resetPalette(); } /** * Get the GUI (palettes) for the layers. The BufferedLayer actually creates * a JTabbedPane holding the palettes for all of its layers, and also has a * pane for itself that provides visibility control for the group layers. */ public Component getGUI() { if (panel == null) { Component[] layerComps = getLayers(); panel = new JPanel(); GridBagLayout pGridbag = new GridBagLayout(); GridBagConstraints pC = new GridBagConstraints(); pC.fill = GridBagConstraints.BOTH; pC.weightx = 1.0f; pC.weighty = 1.0f; panel.setLayout(pGridbag); JTabbedPane tabs = new JTabbedPane(); pGridbag.setConstraints(tabs, pC); panel.add(tabs); JPanel bfPanel = new JPanel(); GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; c.weightx = 0.0f; c.weighty = 0.0f; c.anchor = GridBagConstraints.CENTER; c.gridwidth = GridBagConstraints.REMAINDER; bfPanel.setLayout(gridbag); tabs.addTab("Layer Visibility", bfPanel); for (int i = 0; i < layerComps.length; i++) { Layer layer = (Layer) layerComps[i]; Component layerGUI = layer.getGUI(); if (layerGUI != null) { tabs.addTab(layer.getName(), layerGUI); } VisHelper layerVisibility = new VisHelper(layer); gridbag.setConstraints(layerVisibility, c); bfPanel.add(layerVisibility); } } return panel; } public void paint(Graphics g) { if (hasLayers()) { mapBean.paintChildren(g); } mapBean.paintPainters(g); } /** * Class that helps track turning on/off layers in the buffered layer. */ protected class VisHelper extends JCheckBox implements ActionListener { private static final long serialVersionUID = 1L; Layer layer; public VisHelper(Layer l) { super(l.getName(), l.isVisible()); super.addActionListener(this); layer = l; } public void actionPerformed(ActionEvent ae) { layer.setVisible(((JCheckBox) ae.getSource()).isSelected()); if (logger.isLoggable(Level.FINE)) { logger.fine("Turning " + layer.getName() + (((JCheckBox) ae.getSource()).isSelected() ? " on" : " off")); } layer.repaint(); } } /** * Part of the ProjectionPainter interface. The group layers are given the * projection and the graphics to paint into. */ public void renderDataForProjection(Projection proj, Graphics g) { Component[] layersComps = mapBean.getComponents(); for (int i = layersComps.length - 1; i >= 0; i--) { Layer layer = (Layer) layersComps[i]; if (layer.isVisible()) { layer.renderDataForProjection(proj, g); } } } /** * PropertyChangeListener method, to listen for the source map's background * changes. Act on if necessary. */ public void propertyChange(PropertyChangeEvent pce) { if (pce.getPropertyName() == MapBean.BackgroundProperty) { mapBean.setBckgrnd((Paint) pce.getNewValue()); } } public void dispose() { if (mapBean != null) { mapBean.dispose(); } if (panel != null) { panel.removeAll(); panel = null; } } public void setBufferDirty(boolean value) { mapBean.setBufferDirty(value); } /** * An simple extension of the BufferedMapBean that calls a layer, presumably * its parent, to call repaint(). This is necessary in order to make sure * Swing calls paint properly. Only repaint() is overridden in this class * over a standard BufferedMapBean. */ public class BLMapBean extends BufferedMapBean { private static final long serialVersionUID = 1L; /** * Default constructor. */ public BLMapBean() { super(false); background = OMColor.ALMOST_CLEAR; } /** * For the Buffered Layer MapBean, the background color is always clear. * Let the layers add the color... * * @return color java.awt.Color. */ public Color getBackground() { return OMColor.ALMOST_CLEAR; } /** * Set the buffer dirty, and call repaint on the layer. */ public void repaint(Layer layer) { super.setBufferDirty(true); BufferedLayer.this.repaint(); } /** * Fine-tuned for the purposes of a BufferedLayer - just create a new * image if the buffer is dirty. */ public void paintChildren(Graphics g, Rectangle clip) { BufferedImage localDrawingBuffer = drawingBuffer; if (bufferDirty) { // Reset will clear out the pixels if the size of the buffer is // appropriate for the projection localDrawingBuffer = resetDrawingBuffer(localDrawingBuffer, getProjection()); // We need to draw the projection background with oh-so-slight // transparent rect to allow semi-transparent layers to render // properly. drawProjectionBackground(localDrawingBuffer.getGraphics()); /** * We used to call paintLayers here. */ // paintLayers(localDrawingBuffer.getGraphics()); /** * But, there are problems with rendering if the projection * changes quickly (rotation, following a track, etc) and * there's an animated layer active. Calling * renderDataForProjection on the background layer eliminates * flickering with background layers and the background color. * Sometimes having a BufferedImageRenderPolicy on some of these * background layers also causes flashing. Since the * BufferedLayer provides buffering, those layers can have a * StandardRenderPolicy. */ renderDataForProjection(getProjection(), localDrawingBuffer.getGraphics()); // Reassign the drawingBuffer if a new buffer was allocated. drawingBuffer = localDrawingBuffer; } if (localDrawingBuffer != null) { g.drawImage(localDrawingBuffer, 0, 0, null); } } /** * @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; /** * The original method in the superclass sets the rotation angle on * the projection, but the rotation in this map bean is 0, so * calling this just messes up rotation settings in the projection, * stomping it to zero. This has the effect of breaking * anti-rotation in OMText and OMScalingIcons. */ // ((Proj) proj).setRotationAngle(getRotationAngle()); return proj; } /** * We don't want the BLMapBean to be hanging on to removed layers, * that'll be done at a higher level. */ public void componentRemoved(ContainerEvent e) { super.componentRemoved(e); if (removedLayers != null) { removedLayers.clear(); } } } }