/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI * for visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.workbench.model; import java.awt.Color; import java.util.*; import javax.swing.SwingUtilities; import com.vividsolutions.jts.util.Assert; import com.vividsolutions.jump.feature.FeatureCollection; import com.vividsolutions.jump.feature.FeatureCollectionWrapper; import com.vividsolutions.jump.feature.FeatureDataset; import com.vividsolutions.jump.io.datasource.DataSourceQuery; import com.vividsolutions.jump.util.Blackboard; import com.vividsolutions.jump.workbench.ui.plugin.AddNewLayerPlugIn; import com.vividsolutions.jump.workbench.ui.renderer.style.*; /** * Adds colour, line-width, and other stylistic information to a Feature * Collection. * <p> * When adding or removing multiple features to this Layer's FeatureCollection, * prefer #addAll and #removeAll to #add and #remove -- fewer events will be * fired. */ public class Layer extends AbstractLayerable implements LayerManagerProxy { public static final String FIRING_APPEARANCE_CHANGED_ON_ATTRIBUTE_CHANGE = Layer.class .getName() + " - FIRING APPEARANCE CHANGED ON ATTRIBUTE CHANGE"; private String description = ""; private boolean drawingLast = false; private FeatureCollectionWrapper featureCollectionWrapper; private ArrayList styles = new ArrayList(); private boolean synchronizingLineColor = true; private boolean editable = false; private boolean selectable = true; private boolean readonly = false; private LayerListener layerListener = null; private Blackboard blackboard = new Blackboard() { private static final long serialVersionUID = 6504993615735124204L; { put(FIRING_APPEARANCE_CHANGED_ON_ATTRIBUTE_CHANGE, true); } }; private boolean featureCollectionModified = false; private DataSourceQuery dataSourceQuery; /** * Called by Java2XML */ public Layer() { } public Layer(String name, Color fillColor, FeatureCollection featureCollection, LayerManager layerManager) { super(name, layerManager); Assert.isTrue(featureCollection != null); //Can't fire events because this Layerable hasn't been added to the //LayerManager yet. [Jon Aquino] boolean firingEvents = layerManager.isFiringEvents(); layerManager.setFiringEvents(false); try { addStyle(new BasicStyle()); addStyle(new SquareVertexStyle()); addStyle(new LabelStyle()); } finally { layerManager.setFiringEvents(firingEvents); } getBasicStyle().setFillColor(fillColor); getBasicStyle().setLineColor(defaultLineColor(fillColor)); getBasicStyle().setAlpha(150); setFeatureCollection(featureCollection); } /** * @return a darker version of the given fill colour, for use as the line * colour */ public static Color defaultLineColor(Color fillColor) { return fillColor.darker(); } public void setDescription(String description) { Assert.isTrue( description != null, "Java2XML requires that the description be non-null. Use an empty string if necessary."); this.description = description; } /** * @return true if this layer should always be 'readonly' I.e.: The layer * should never have the editable field set to true. */ public boolean isReadonly() { return readonly; } /** * Set whether this layer can be made editable. */ public void setReadonly( boolean value ) { readonly = value; } /** * @return true if features in this layer can be selected. */ public boolean isSelectable() { return selectable; } /** * Set whether or not features in this layer can be selected. * @param value true if features in this layer can be selected */ public void setSelectable( boolean value ) { selectable = value; } /** * Used for lightweight layers like the Vector layer. * * @param drawingLast * true if the layer should be among those drawn last */ public void setDrawingLast(boolean drawingLast) { this.drawingLast = drawingLast; fireAppearanceChanged(); } public void setFeatureCollection(final FeatureCollection featureCollection) { final FeatureCollection oldFeatureCollection = featureCollectionWrapper != null ? featureCollectionWrapper .getUltimateWrappee() : AddNewLayerPlugIn.createBlankFeatureCollection(); ObservableFeatureCollection observableFeatureCollection = new ObservableFeatureCollection( featureCollection); observableFeatureCollection.checkNotWrappingSameClass(); observableFeatureCollection .add(new ObservableFeatureCollection.Listener() { public void featuresAdded(Collection features) { getLayerManager().fireFeaturesChanged(features, FeatureEventType.ADDED, Layer.this); } public void featuresRemoved(Collection features) { getLayerManager().fireFeaturesChanged(features, FeatureEventType.DELETED, Layer.this); } }); if ((getLayerManager() != null) && getLayerManager().getLayers().contains(this)) { //Don't fire APPEARANCE_CHANGED immediately, to avoid the //following problem: //(1) Add fence layer //(2) LAYER_ADDED event will be called //(3) APPEARANCE_CHANGED will be fired in this method (before //the JTree receives its LAYER_ADDED event) //(4) The JTree will complain because it gets the // APPEARANCE_CHANGED //event before the LAYER_ADDED event: // java.lang.ArrayIndexOutOfBoundsException: 0 >= 0 // at java.util.Vector.elementAt(Vector.java:412) // at // javax.swing.tree.DefaultMutableTreeNode.getChildAt(DefaultMutableTreeNode.java:226) // at // javax.swing.tree.VariableHeightLayoutCache.treeNodesChanged(VariableHeightLayoutCache.java:369) // at // javax.swing.plaf.basic.BasicTreeUI$TreeModelHandler.treeNodesChanged(BasicTreeUI.java:2339) // at // javax.swing.tree.DefaultTreeModel.fireTreeNodesChanged(DefaultTreeModel.java:435) // at // javax.swing.tree.DefaultTreeModel.nodesChanged(DefaultTreeModel.java:318) // at // javax.swing.tree.DefaultTreeModel.nodeChanged(DefaultTreeModel.java:251) // at // com.vividsolutions.jump.workbench.model.LayerTreeModel.layerChanged(LayerTreeModel.java:292) //[Jon Aquino] SwingUtilities.invokeLater(new Runnable() { public void run() { //Changed APPEARANCE_CHANGED event to FEATURE_DELETED and //FEATURE_ADDED events, but I think the lengthy comment //above still applies. [Jon Aquino] //Drop #isEmpty checks, so that database-backed feature //collections don't have to implement it. //[Jon Aquino 2005-03-02] getLayerManager().fireFeaturesChanged( oldFeatureCollection.getFeatures(), FeatureEventType.DELETED, Layer.this); getLayerManager().fireFeaturesChanged( featureCollection.getFeatures(), FeatureEventType.ADDED, Layer.this); } }); } setFeatureCollectionWrapper(observableFeatureCollection); } /** * Editability is not enforced; all parties are responsible for heeding this * flag. */ public void setEditable(boolean editable) { if (this.editable == editable) { return; } this.editable = editable; fireLayerChanged(LayerEventType.METADATA_CHANGED); } public boolean isEditable() { return editable; } public void setSynchronizingLineColor(boolean synchronizingLineColor) { this.synchronizingLineColor = synchronizingLineColor; fireAppearanceChanged(); } public BasicStyle getBasicStyle() { return (BasicStyle) getStyle(BasicStyle.class); } public VertexStyle getVertexStyle() { return (VertexStyle) getStyle(VertexStyle.class); } public LabelStyle getLabelStyle() { return (LabelStyle) getStyle(LabelStyle.class); } public String getDescription() { return description; } /** * Returns a wrapper around the FeatureCollection which was added using * #wrapFeatureCollection. The original FeatureCollection can be retrieved * using FeatureCollectionWrapper#getWrappee. However, parties are * encouraged to use the FeatureCollectionWrapper instead, so that feature * additions and removals cause FeatureEvents to be fired (by the Layer). */ public FeatureCollectionWrapper getFeatureCollectionWrapper() { return featureCollectionWrapper; } protected void setFeatureCollectionWrapper( FeatureCollectionWrapper featureCollectionWrapper) { this.featureCollectionWrapper = featureCollectionWrapper; } /** * Styles do not notify the Layer when their parameters change. Therefore, * after you modify a Style's parameters (for example, the fill colour of * BasicStyle), be sure to call #fireAppearanceChanged * * @param c * Can even be the desired Style's superclass or interface * @return The style value */ public Style getStyle(Class c) { for (Iterator i = styles.iterator(); i.hasNext();) { Style p = (Style) i.next(); if (c.isInstance(p)) { return p; } } return null; } public List getStyles() { return Collections.unmodifiableList(styles); } public boolean hasReadableDataSource() { return dataSourceQuery != null && dataSourceQuery.getDataSource().isReadable(); } public boolean isDrawingLast() { return drawingLast; } public boolean isSynchronizingLineColor() { return synchronizingLineColor; } public void addStyle(Style style) { styles.add(style); fireAppearanceChanged(); } /** * Releases references to the data, to facilitate garbage collection. * Important for MDI apps like the JUMP Workbench. Called when the last * JInternalFrame viewing the LayerManager is closed (i.e. internal frame's * responsibility). To conserve memory, if layers are frequently added and * removed from the LayerManager, parties may want to call #dispose * themselves rather than waiting for the internal frame to be closed. */ public void dispose() { //Don't just call FeatureCollection#removeAll, because it may be a // database //table, and we don't want to delete its contents! [Jon Aquino] setFeatureCollection(AddNewLayerPlugIn.createBlankFeatureCollection()); } public void removeStyle(Style p) { Assert.isTrue(styles.remove(p)); fireAppearanceChanged(); } public Collection cloneStyles() { ArrayList styleClones = new ArrayList(); for (Iterator i = getStyles().iterator(); i.hasNext();) { Style style = (Style) i.next(); styleClones.add(style.clone()); } return styleClones; } public void setStyles(Collection newStyles) { boolean firingEvents = getLayerManager().isFiringEvents(); getLayerManager().setFiringEvents(false); try { //new ArrayList to prevent ConcurrentModificationException [Jon // Aquino] for (Iterator i = new ArrayList(getStyles()).iterator(); i .hasNext();) { Style style = (Style) i.next(); removeStyle(style); } for (Iterator i = newStyles.iterator(); i.hasNext();) { Style style = (Style) i.next(); addStyle(style); } } finally { getLayerManager().setFiringEvents(firingEvents); } fireAppearanceChanged(); } public void setLayerManager(LayerManager layerManager) { if (layerManager != null) { layerManager.removeLayerListener(getLayerListener()); } super.setLayerManager(layerManager); layerManager.addLayerListener(getLayerListener()); } private LayerListener getLayerListener() { //Need to create layerListener lazily because it will be called by the //superclass constructor. [Jon Aquino] if (layerListener == null) { layerListener = new LayerListener() { public void featuresChanged(FeatureEvent e) { if (e.getLayer() == Layer.this) { setFeatureCollectionModified(true); //Before I wasn't firing appearance-changed on an // attribute //change. But now with labelling and colour theming, //I have to. [Jon Aquino] if (e.getType() != FeatureEventType.ATTRIBUTES_MODIFIED || getBlackboard() .get( FIRING_APPEARANCE_CHANGED_ON_ATTRIBUTE_CHANGE, true)) { //Fixed bug above -- wasn't supplying a default // value to //Blackboard#getBoolean, resulting in a // NullPointerException //when the Layer was created using the // parameterless //constructor (because that constructor doesn't // initialize //FIRING_APPEARANCE_CHANGED_ON_ATTRIBUTE_CHANGE //on the blackboard [Jon Aquino 10/21/2003] fireAppearanceChanged(); } } } public void layerChanged(LayerEvent e) { } public void categoryChanged(CategoryEvent e) { } }; } return layerListener; } public Blackboard getBlackboard() { return blackboard; } /** * Enables a layer to be changed undoably. Since the layer's features are * saved, only use this method for layers with few features. */ public static UndoableCommand addUndo(final String layerName, final LayerManagerProxy proxy, final UndoableCommand wrappeeCommand) { return new UndoableCommand(wrappeeCommand.getName()) { private Layer layer; private String categoryName; private Collection features; private boolean visible; private Layer currentLayer() { return proxy.getLayerManager().getLayer(layerName); } public void execute() { layer = currentLayer(); if (layer != null) { features = new ArrayList(layer .getFeatureCollectionWrapper().getFeatures()); categoryName = layer.getName(); visible = layer.isVisible(); } wrappeeCommand.execute(); } public void unexecute() { wrappeeCommand.unexecute(); if ((layer == null) && (currentLayer() != null)) { proxy.getLayerManager().remove(currentLayer()); } if ((layer != null) && (currentLayer() == null)) { proxy.getLayerManager().addLayer(categoryName, layer); } if (layer != null) { layer.getFeatureCollectionWrapper().clear(); layer.getFeatureCollectionWrapper().addAll(features); layer.setVisible(visible); } } }; } /** * Does nothing if the underlying feature collection is not a * FeatureDataset. */ public static void tryToInvalidateEnvelope(Layer layer) { if (layer.getFeatureCollectionWrapper().getUltimateWrappee() instanceof FeatureDataset) { ((FeatureDataset) layer.getFeatureCollectionWrapper() .getUltimateWrappee()).invalidateEnvelope(); } } public DataSourceQuery getDataSourceQuery() { return dataSourceQuery; } public Layer setDataSourceQuery(DataSourceQuery dataSourceQuery) { this.dataSourceQuery = dataSourceQuery; return this; } public boolean isFeatureCollectionModified() { return featureCollectionModified; } public Layer setFeatureCollectionModified(boolean featureCollectionModified) { if (this.featureCollectionModified == featureCollectionModified) { return this; } this.featureCollectionModified = featureCollectionModified; fireLayerChanged(LayerEventType.METADATA_CHANGED); return this; } }