/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* 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 3 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, see http://www.gnu.org/licenses/
*/
package com.bc.ceres.glayer;
import com.bc.ceres.binding.Property;
import com.bc.ceres.binding.PropertyContainer;
import com.bc.ceres.binding.PropertySet;
import com.bc.ceres.core.Assert;
import com.bc.ceres.core.ExtensibleObject;
import com.bc.ceres.grender.Rendering;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.MessageFormat;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
// todo - make this class thread safe!!!
/**
* A layer contributes graphical elements to a drawing represented by a {@link com.bc.ceres.grender.Rendering}.
*
* @author Marco Peters
* @author Norman Fomferra
* @version $revision$ $date$
*/
public abstract class Layer extends ExtensibleObject {
private static volatile AtomicInteger instanceCount = new AtomicInteger(0);
private final LayerType layerType;
private Layer parent;
private final LayerList children;
private String id;
private String name;
private boolean visible;
private double transparency;
private Composite composite;
private transient final ArrayList<LayerListener> layerListenerList;
private transient final ConfigurationPCL configurationPCL;
private final PropertySet configuration;
protected Layer(LayerType layerType) {
this(layerType, new PropertyContainer());
}
/**
* Constructor. The following default properties are used:
* <ul>
* <li>{@code name = getClass().getName()}</li>
* <li>{@code visible = true}</li>
* <li>{@code transparency = 0.0}</li>
* </ul>
*
* @param layerType the layer type.
* @param configuration the configuration used by the layer type to create this layer.
*/
protected Layer(LayerType layerType, PropertySet configuration) {
this.configuration = configuration;
Assert.notNull(layerType, "layerType");
this.layerType = layerType;
this.name = createDefaultName(layerType);
this.parent = null;
this.id = Long.toHexString(System.nanoTime() + (instanceCount.incrementAndGet()));
this.children = new LayerList();
this.visible = true;
this.transparency = 0.0;
this.composite = Composite.SRC_OVER;
this.layerListenerList = new ArrayList<LayerListener>(8);
this.configurationPCL = new ConfigurationPCL();
configuration.addPropertyChangeListener(configurationPCL);
}
/**
* @return The layer type.
*/
public LayerType getLayerType() {
return layerType;
}
/**
* Returns the configuration which can be used by the layer type to recreate this layer.
*
* @return the configuration.
*/
public PropertySet getConfiguration() {
return configuration;
}
/**
* @return The parent layer, or {@code null} if this layer is not a child of any other layer.
*/
public Layer getParent() {
return parent;
}
/**
* @return true, if this layer is a collection of other layers.
*/
public boolean isCollectionLayer() {
return false;
}
/**
* Gets the child layers of this layer. The returned list is "life", modifying the list's content
* will cause this layer to fire change events.
*
* @return The child layers of this layer. May be empty.
*/
public List<Layer> getChildren() {
return children;
}
/**
* @return The name.
*/
public String getName() {
return name;
}
/**
* @param name The name.
*/
public void setName(String name) {
Assert.notNull(name, "name");
final String oldValue = this.name;
if (!oldValue.equals(name)) {
this.name = name;
fireLayerPropertyChanged("name", oldValue, this.name);
}
}
/**
* @return An identifier which can be used to search for special layers.
*
* @since Ceres 0.9
*/
public String getId() {
return id;
}
/**
* @param id An identifier which can be used to search for special layers.
*
* @since Ceres 0.9
*/
public void setId(String id) {
Assert.notNull(id, "id");
this.id = id;
}
/**
* Gets the index of the first child layer having the given identifier.
*
* @param id The identifier.
*
* @return The child index, or {@code -1} if no such layer exists.
*
* @since Ceres 0.9
*/
public int getChildIndex(String id) {
Assert.notNull(id, "id");
for (int i = 0; i < children.size(); i++) {
Layer child = children.get(i);
if (id.equals(child.getId())) {
return i;
}
}
return -1;
}
/**
* @return {@code true}, if this layer is visible.
*/
public boolean isVisible() {
return visible;
}
/**
* @param visible {@code true}, if this layer is visible.
*/
public void setVisible(boolean visible) {
final boolean oldValue = this.visible;
if (oldValue != visible) {
this.visible = visible;
fireLayerPropertyChanged("visible", oldValue, this.visible);
}
}
/**
* Returns the transparency of this layer.
*
* @return the transparency of this layer.
*/
public double getTransparency() {
return transparency;
}
/**
* Sets the transparency of this layer to a new value.
*
* @param transparency the new transparency value of this layer.
*/
public void setTransparency(double transparency) {
final double oldValue = this.transparency;
if (oldValue != transparency) {
this.transparency = transparency;
fireLayerPropertyChanged("transparency", oldValue, this.transparency);
}
}
/**
* Returns the composite of this layer.
*
* @return the composite of this layer.
*/
public Composite getComposite() {
return composite;
}
/**
* Sets the composite of this layer.
*
* @param composite the new composite of this layer.
*/
public void setComposite(Composite composite) {
final Composite oldValue = this.composite;
if (oldValue != composite) {
this.composite = composite;
fireLayerPropertyChanged("composite", oldValue, this.composite);
}
}
protected final <T> T getConfigurationProperty(String propertyName, T defaultValue) {
T value = defaultValue;
final PropertySet configuration = getConfiguration();
final Property model = configuration.getProperty(propertyName);
if (model != null) {
final Class<?> expectedType = defaultValue.getClass();
final Class<?> descriptorType = model.getDescriptor().getType();
if (expectedType.isAssignableFrom(descriptorType)) {
if (model.getValue() != null) {
//noinspection unchecked
value = (T) model.getValue();
} else {
if (model.getDescriptor().getDefaultValue() != null) {
//noinspection unchecked
value = (T) model.getDescriptor().getDefaultValue();
}
}
} else {
throw new IllegalArgumentException(MessageFormat.format(
"Class ''{0}'' is not assignable from class ''{1}''.", expectedType, descriptorType));
}
}
return value;
}
/**
* Gets the model bounds (bounding box) of the layer in model coordinates.
* The default implementation returns the union of the model bounds (if any) returned by
* {@link #getLayerModelBounds()} and {@link #getChildrenModelBounds()}.
*
* @return The bounds of the layer in model coordinates or {@code null} if this layer
* and all children have no specified boundary.
*/
public final Rectangle2D getModelBounds() {
final Rectangle2D layerBounds = getLayerModelBounds();
final Rectangle2D childrenBounds = getChildrenModelBounds();
if (layerBounds == null) {
return childrenBounds;
} else if (childrenBounds == null) {
return layerBounds;
} else {
Rectangle2D bounds = (Rectangle2D) layerBounds.clone();
bounds.add(childrenBounds);
return bounds;
}
}
/**
* Gets the bounds (bounding box) of this layer in model coordinates.
* Called by {@link #getModelBounds()}.
* The default implementation returns {@code null}.
*
* @return The bounds of the layer in model coordinates or {@code null} if this layer
* has no specified boundary.
*/
protected Rectangle2D getLayerModelBounds() {
return null;
}
/**
* Gets the bounds (bounding box) of the child layers in model coordinates.
* Called by {@link #getModelBounds()}.
* The default implementation returns the union bounds (if any) of all child layers.
*
* @return The bounds of the child layers in model coordinates or {@code null}
* none of the children have a specified boundary.
*/
protected Rectangle2D getChildrenModelBounds() {
Rectangle2D bounds = null;
for (Layer layer : children) {
Rectangle2D childBounds = layer.getModelBounds();
if (childBounds != null) {
if (bounds == null) {
bounds = (Rectangle2D) childBounds.clone();
} else {
bounds.add(childBounds);
}
}
}
return bounds;
}
/**
* Renders the layer. Calls {@code render(rendering,null)}.
*
* @param rendering The rendering to which the layer will be rendered.
*
* @see #render(com.bc.ceres.grender.Rendering, LayerFilter)
*/
public final void render(Rendering rendering) {
render(rendering, null);
}
/**
* Renders the layer. The base class implementation configures the rendering with respect to the
* "transparency" and "composite" style properties. Then
* {@link #renderLayer(com.bc.ceres.grender.Rendering)} followed by
* {@link #renderChildren(com.bc.ceres.grender.Rendering, LayerFilter)} are called.
*
* @param rendering The rendering to which the layer will be rendered.
* @param filter An optional layer filter. May be {@code null}.
*/
public final void render(Rendering rendering, LayerFilter filter) {
final double transparency = getTransparency();
if (!isVisible() || transparency == 1.0) {
return;
}
final Graphics2D g = rendering.getGraphics();
java.awt.Composite oldComposite = null;
try {
if (transparency > 0.0) {
oldComposite = g.getComposite();
g.setComposite(getComposite().getAlphaComposite((float) (1.0 - transparency)));
}
if (filter == null) {
renderLayer(rendering);
renderChildren(rendering, null);
} else {
if (filter.accept(this)) {
renderLayer(rendering);
}
renderChildren(rendering, filter);
}
} finally {
if (oldComposite != null) {
g.setComposite(oldComposite);
}
}
}
/**
* Renders the layer. Called by {@link #render(com.bc.ceres.grender.Rendering)}.
* The default implementation does nothing.
*
* @param rendering The rendering to which the layer will be rendered.
*/
protected void renderLayer(Rendering rendering) {
}
/**
* Renders the child layers of this layer. Called by {@link #render(com.bc.ceres.grender.Rendering)}.
* The default implementation calls {@link #render(com.bc.ceres.grender.Rendering)} on all child layers.
*
* @param rendering The rendering to which the layer will be rendered.
* @param filter A layer filter. May be {@code null}.
*/
protected void renderChildren(Rendering rendering, LayerFilter filter) {
for (int i = children.size() - 1; i >= 0; --i) {
children.get(i).render(rendering, filter);
}
}
/**
* Disposes all allocated resources. Called if the layer will no longer be in use.
* The default implementation removes all registered listeners,
* calls {@link #disposeChildren()} followed by {@link #disposeLayer()}.
*/
public final void dispose() {
configuration.removePropertyChangeListener(configurationPCL);
layerListenerList.clear();
disposeChildren();
disposeLayer();
}
/**
* Disposes the layer. Called by {@link #dispose()}.
* The default implementation does nothing.
*/
protected void disposeLayer() {
}
/**
* Disposes the child layers of this layer. Called by {@link #dispose()}.
* The default implementation calls {@link #dispose()} on all child layers
* and removes them from this layer.
*/
protected void disposeChildren() {
children.dispose();
}
/**
* Adds a change listener to this layer.
*
* @param listener The listener.
*/
public void addListener(LayerListener listener) {
Assert.notNull(listener, "listener");
if (!layerListenerList.contains(listener)) {
layerListenerList.add(listener);
}
}
/**
* Removes a change listener from this layer.
*
* @param listener The listener.
*/
public void removeListener(LayerListener listener) {
layerListenerList.remove(listener);
}
/**
* @return The listeners added to this layer..
*/
public LayerListener[] getListeners() {
return layerListenerList.toArray(new LayerListener[layerListenerList.size()]);
}
/**
* @return This layer's listeners plus the ones of all parent layers.
*/
LayerListener[] getReachableListeners() {
ArrayList<LayerListener> list = new ArrayList<LayerListener>(16);
list.addAll(Arrays.asList(getListeners()));
Layer currentParent = getParent();
while (currentParent != null) {
list.addAll(Arrays.asList(currentParent.getListeners()));
currentParent = currentParent.getParent();
}
return list.toArray(new LayerListener[list.size()]);
}
protected void fireLayerPropertyChanged(String propertyName, Object oldValue, Object newValue) {
fireLayerPropertyChanged(new PropertyChangeEvent(this, propertyName, oldValue, newValue));
}
protected void fireLayerPropertyChanged(PropertyChangeEvent event) {
for (LayerListener listener : getReachableListeners()) {
listener.handleLayerPropertyChanged(this, event);
}
}
protected void fireLayerDataChanged(Rectangle2D modelRegion) {
for (LayerListener listener : getReachableListeners()) {
listener.handleLayerDataChanged(this, modelRegion);
}
}
protected void fireLayersAdded(Layer[] layers) {
for (LayerListener listener : getReachableListeners()) {
listener.handleLayersAdded(this, layers);
}
}
protected void fireLayersRemoved(Layer[] layers) {
for (LayerListener listener : getReachableListeners()) {
listener.handleLayersRemoved(this, layers);
}
}
@Override
public String toString() {
return getName();
}
/**
* @param parent The parent layer, or {@code null} if this layer has no parent.
*/
void setParent(Layer parent) {
this.parent = parent;
}
/**
* Regenerates the layer. May be called to update the layer data.
* The default implementation does nothing.
*/
public void regenerate() {
}
private class ConfigurationPCL implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent event) {
fireLayerPropertyChanged(event);
}
}
private static String createDefaultName(LayerType layerType) {
String name = layerType.getClass().getSimpleName();
final String suffix = "Type";
if (name.endsWith(suffix)) {
return name.substring(0, name.length() - suffix.length());
}
return name;
}
/**
* A change-aware list of layers.
*/
private class LayerList extends AbstractList<Layer> {
private final List<Layer> layerList;
LayerList() {
this.layerList = new Vector<Layer>(8);
}
@Override
public int size() {
return layerList.size();
}
@Override
public Layer get(int i) {
return layerList.get(i);
}
@Override
public Layer set(int i, Layer layer) {
final Layer oldLayer = layerList.set(i, layer);
if (layer != oldLayer) {
oldLayer.setParent(null);
layer.setParent(Layer.this);
fireLayersRemoved(new Layer[]{oldLayer});
fireLayersAdded(new Layer[]{layer});
}
return oldLayer;
}
@Override
public void add(int i, Layer layer) {
layerList.add(i, layer);
layer.setParent(Layer.this);
fireLayersAdded(new Layer[]{layer});
}
@Override
public boolean remove(Object o) {
synchronized (layerList) {
final int i = indexOf(o);
if (i != -1) {
return remove(i) == o;
}
return false;
}
}
@Override
public Layer remove(int i) {
final Layer layer = layerList.remove(i);
layer.setParent(null);
fireLayersRemoved(new Layer[]{layer});
return layer;
}
@Override
public <T> T[] toArray(T[] array) {
return layerList.toArray(array);
}
private void dispose() {
synchronized (layerList) {
final Layer[] layers = layerList.toArray(new Layer[layerList.size()]);
layerList.clear();
for (Layer layer : layers) {
layer.dispose();
layer.setParent(null);
}
}
}
}
}