/*
* 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.support;
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.glayer.Layer;
import com.bc.ceres.glayer.LayerContext;
import com.bc.ceres.glayer.LayerType;
import com.bc.ceres.glayer.LayerTypeRegistry;
import com.bc.ceres.glayer.annotations.LayerTypeMetadata;
import com.bc.ceres.glevel.MultiLevelModel;
import com.bc.ceres.glevel.MultiLevelRenderer;
import com.bc.ceres.glevel.MultiLevelSource;
import com.bc.ceres.glevel.support.ConcurrentMultiLevelRenderer;
import com.bc.ceres.glevel.support.DefaultMultiLevelModel;
import com.bc.ceres.glevel.support.DefaultMultiLevelRenderer;
import com.bc.ceres.glevel.support.DefaultMultiLevelSource;
import com.bc.ceres.grender.InteractiveRendering;
import com.bc.ceres.grender.Rendering;
import com.bc.ceres.grender.Viewport;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.RenderedImage;
/**
* A multi-resolution capable image layer.
*
* @author Norman Fomferra
*/
public class ImageLayer extends Layer {
private static final Type LAYER_TYPE = LayerTypeRegistry.getLayerType(Type.class);
public static final String PROPERTY_NAME_MULTI_LEVEL_SOURCE = "multiLevelSource";
public static final String PROPERTY_NAME_BORDER_SHOWN = "borderShown";
public static final String PROPERTY_NAME_BORDER_WIDTH = "borderWidth";
public static final String PROPERTY_NAME_BORDER_COLOR = "borderColor";
public static final String PROPERTY_NAME_PIXEL_BORDER_SHOWN = "pixelBorderShown";
public static final String PROPERTY_NAME_PIXEL_BORDER_WIDTH = "pixelBorderWidth";
public static final String PROPERTY_NAME_PIXEL_BORDER_COLOR = "pixelBorderColor";
public static final boolean DEFAULT_BORDER_SHOWN = false;
public static final Color DEFAULT_BORDER_COLOR = new Color(204, 204, 255);
public static final double DEFAULT_BORDER_WIDTH = 1.0;
public static final Boolean DEFAULT_PIXEL_BORDER_SHOWN = true;
public static final Color DEFAULT_PIXEL_BORDER_COLOR = new Color(255, 255, 204);
public static final double DEFAULT_PIXEL_BORDER_WIDTH = 0.0;
private static final double MIN_PIXEL_SIZE_IN_VIEW = 16.0;
/**
* @deprecated since BEAM 4.7, no replacement; kept for compatibility of sessions
*/
@Deprecated
private static final String PROPERTY_NAME_IMAGE_TO_MODEL_TRANSFORM = "imageToModelTransform";
private MultiLevelSource multiLevelSource;
private MultiLevelRenderer multiLevelRenderer;
/**
* Constructs a single-resolution-level image layer.
*
* @param image the image
*/
public ImageLayer(RenderedImage image) {
this(image, new AffineTransform(), 1);
}
/**
* Constructs a multi-resolution-level image layer.
*
* @param image the image
* @param imageToModelTransform the transformation from image to model CS
* @param levelCount the number of resolution levels
*/
public ImageLayer(RenderedImage image, AffineTransform imageToModelTransform, int levelCount) {
this(new DefaultMultiLevelSource(image,
new DefaultMultiLevelModel(levelCount, imageToModelTransform,
DefaultMultiLevelModel.getModelBounds(
imageToModelTransform, image))
));
}
/**
* Constructs a multi-resolution-level image layer.
*
* @param multiLevelSource the multi-resolution-level image
*/
public ImageLayer(MultiLevelSource multiLevelSource) {
this(LAYER_TYPE, multiLevelSource, initConfiguration(LAYER_TYPE.createLayerConfig(null), multiLevelSource));
}
public ImageLayer(Type layerType, MultiLevelSource multiLevelSource, PropertySet configuration) {
super(layerType, configuration);
Assert.notNull(multiLevelSource);
this.multiLevelSource = multiLevelSource;
setName("Image Layer");
}
@Override
public void regenerate() {
clearCaches();
fireLayerDataChanged(getModelBounds());
}
public RenderedImage getImage() {
return getImage(0);
}
public MultiLevelSource getMultiLevelSource() {
return multiLevelSource;
}
public void setMultiLevelSource(MultiLevelSource multiLevelSource) {
Assert.notNull(multiLevelSource);
if (multiLevelSource != this.multiLevelSource) {
final Rectangle2D region;
final Rectangle2D oldBounds = this.multiLevelSource.getModel().getModelBounds();
final Rectangle2D newBounds = multiLevelSource.getModel().getModelBounds();
if (oldBounds == null) {
region = newBounds;
} else if (newBounds == null) {
region = oldBounds;
} else {
region = oldBounds.createUnion(newBounds);
}
clearCaches();
this.multiLevelSource = multiLevelSource;
multiLevelRenderer = null;
fireLayerDataChanged(region);
}
}
public AffineTransform getImageToModelTransform() {
return getImageToModelTransform(0);
}
public AffineTransform getModelToImageTransform() {
return getModelToImageTransform(0);
}
public RenderedImage getImage(int level) {
return multiLevelSource.getImage(level);
}
public AffineTransform getImageToModelTransform(int level) {
return multiLevelSource.getModel().getImageToModelTransform(level);
}
public AffineTransform getModelToImageTransform(int level) {
return multiLevelSource.getModel().getModelToImageTransform(level);
}
public int getLevel(Viewport vp) {
return getLevel(multiLevelSource.getModel(), vp);
}
public static int getLevel(MultiLevelModel model, Viewport vp) {
final AffineTransform i2m = model.getImageToModelTransform(0);
final double i2mScale = Math.sqrt(Math.abs(i2m.getDeterminant()));
final double m2vScale = 1.0 / vp.getZoomFactor();
final double scale = m2vScale / i2mScale;
return model.getLevel(scale);
}
@Override
protected Rectangle2D getLayerModelBounds() {
return multiLevelSource.getModel().getModelBounds();
}
@Override
protected void renderLayer(Rendering rendering) {
if (multiLevelSource == MultiLevelSource.NULL) {
return;
}
final int level = getLevel(rendering.getViewport());
final MultiLevelRenderer renderer = getRenderer(rendering);
renderer.renderImage(rendering, multiLevelSource, level);
renderImageGridIndicators(rendering, level);
}
private void renderImageGridIndicators(Rendering rendering, int level) {
final boolean pixelBorderShown = level == 0 && isPixelBorderShown();
final boolean imageBorderShown = isBorderShown();
if (!pixelBorderShown && !imageBorderShown) {
return;
}
final Graphics2D graphics2D = rendering.getGraphics();
final Object oldAntialiasing = graphics2D.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
final Paint oldPaint = graphics2D.getPaint();
final Stroke oldStroke = graphics2D.getStroke();
try {
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (pixelBorderShown) {
drawPixelBorders(rendering, graphics2D);
}
if (imageBorderShown) {
drawImageBorder(rendering, graphics2D, level);
}
} finally {
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntialiasing);
graphics2D.setPaint(oldPaint);
graphics2D.setStroke(oldStroke);
}
}
private void drawImageBorder(Rendering rendering, Graphics2D graphics2D, int level) {
final RenderedImage image = multiLevelSource.getImage(level);
final Viewport viewport = rendering.getViewport();
final AffineTransform i2m = multiLevelSource.getModel().getImageToModelTransform(level);
final AffineTransform m2v = viewport.getModelToViewTransform();
// fixme: better concat transforms before (nf)
final Shape modelShape = i2m.createTransformedShape(
new Rectangle(image.getMinX(), image.getMinY(), image.getWidth(), image.getHeight()));
final Shape viewShape = m2v.createTransformedShape(modelShape);
graphics2D.setStroke(new BasicStroke((float) Math.max(0.0, getBorderWidth())));
graphics2D.setColor(getBorderColor());
graphics2D.draw(viewShape);
}
private void drawPixelBorders(Rendering rendering, Graphics2D graphics2D) {
final Viewport viewport = rendering.getViewport();
final AffineTransform m2i0 = multiLevelSource.getModel().getModelToImageTransform(0);
final AffineTransform i2m0 = multiLevelSource.getModel().getImageToModelTransform(0);
final AffineTransform v2m = viewport.getViewToModelTransform();
final AffineTransform m2v = viewport.getModelToViewTransform();
final Rectangle viewBounds = viewport.getViewBounds();
// fixme: better concat transforms before (nf)
final Shape imageShape = m2i0.createTransformedShape(v2m.createTransformedShape(viewBounds));
final Rectangle2D imageBounds = imageShape.getBounds2D();
final double pixelSizeInViewX = i2m0.getScaleX() * m2v.getScaleX();
final double pixelSizeInViewY = i2m0.getScaleY() * m2v.getScaleY();
if (pixelSizeInViewX >= MIN_PIXEL_SIZE_IN_VIEW || pixelSizeInViewY >= MIN_PIXEL_SIZE_IN_VIEW) {
RenderedImage image0 = multiLevelSource.getImage(0);
int x0 = Math.max(0, (int) Math.floor(imageBounds.getX()));
int y0 = Math.max(0, (int) Math.floor(imageBounds.getY()));
int x1 = Math.min(image0.getWidth(), x0 + (int) Math.round(imageBounds.getWidth()) + 1);
int y1 = Math.min(image0.getHeight(), y0 + (int) Math.round(imageBounds.getHeight()) + 1);
// fixme: the dashed stroke is slow (nf)
/*
graphics2D.setStroke(new BasicStroke((float) Math.max(0.0, getPixelBorderWidth()),
BasicStroke.CAP_SQUARE,
BasicStroke.JOIN_MITER,
10.0f, new float[] {3.0F, 3.0F}, 0.0f));
*/
graphics2D.setStroke(new BasicStroke((float) Math.max(0.0, getPixelBorderWidth())));
graphics2D.setColor(getPixelBorderColor());
for (int x = x0; x <= x1; x++) {
// fixme: better concat transforms before (nf)
graphics2D.draw(m2v.createTransformedShape(i2m0.createTransformedShape(new Line2D.Double(x, y0, x, y1))));
}
for (int y = y0; y <= y1; y++) {
// fixme: better concat transforms before (nf)
graphics2D.draw(m2v.createTransformedShape(i2m0.createTransformedShape(new Line2D.Double(x0, y, x1, y))));
}
}
}
@Override
protected synchronized void disposeLayer() {
resetRenderer();
if (multiLevelSource != null) {
multiLevelSource.reset();
multiLevelSource = null;
}
}
private synchronized MultiLevelRenderer getRenderer(Rendering rendering) {
if (rendering instanceof InteractiveRendering) {
if (multiLevelRenderer == null) {
multiLevelRenderer = new ConcurrentMultiLevelRenderer();
// multiLevelRenderer = new DefaultMultiLevelRenderer();
}
return multiLevelRenderer;
} else {
return new DefaultMultiLevelRenderer();
}
}
private void resetRenderer() {
if (multiLevelRenderer != null) {
multiLevelRenderer.reset();
multiLevelRenderer = null;
}
}
private void clearCaches() {
resetRenderer();
multiLevelSource.reset();
}
public boolean isBorderShown() {
return getConfigurationProperty(PROPERTY_NAME_BORDER_SHOWN, DEFAULT_BORDER_SHOWN);
}
public double getBorderWidth() {
return getConfigurationProperty(PROPERTY_NAME_BORDER_WIDTH, DEFAULT_BORDER_WIDTH);
}
public Color getBorderColor() {
return getConfigurationProperty(PROPERTY_NAME_BORDER_COLOR, DEFAULT_BORDER_COLOR);
}
public boolean isPixelBorderShown() {
return getConfigurationProperty(PROPERTY_NAME_PIXEL_BORDER_SHOWN, DEFAULT_PIXEL_BORDER_SHOWN);
}
public double getPixelBorderWidth() {
return getConfigurationProperty(PROPERTY_NAME_PIXEL_BORDER_WIDTH, DEFAULT_PIXEL_BORDER_WIDTH);
}
public Color getPixelBorderColor() {
return getConfigurationProperty(PROPERTY_NAME_PIXEL_BORDER_COLOR, DEFAULT_PIXEL_BORDER_COLOR);
}
private static PropertySet initConfiguration(PropertySet configuration, MultiLevelSource multiLevelSource) {
configuration.setValue(PROPERTY_NAME_MULTI_LEVEL_SOURCE, multiLevelSource);
return configuration;
}
@LayerTypeMetadata(name = "ImageLayerType",
aliasNames = {"com.bc.ceres.glayer.support.ImageLayer$Type"})
public static class Type extends LayerType {
@Override
public boolean isValidFor(LayerContext ctx) {
return true;
}
@Override
public Layer createLayer(LayerContext ctx, PropertySet configuration) {
MultiLevelSource multiLevelSource = (MultiLevelSource) configuration.getValue(
ImageLayer.PROPERTY_NAME_MULTI_LEVEL_SOURCE);
return new ImageLayer(this, multiLevelSource, configuration);
}
@Override
public PropertySet createLayerConfig(LayerContext ctx) {
final PropertyContainer template = new PropertyContainer();
addMultiLevelSourceModel(template);
addImageToModelTransformModel(template);
template.addProperty(Property.create(ImageLayer.PROPERTY_NAME_BORDER_SHOWN, Boolean.class, ImageLayer.DEFAULT_BORDER_SHOWN, true));
template.addProperty(Property.create(ImageLayer.PROPERTY_NAME_BORDER_COLOR, Color.class, ImageLayer.DEFAULT_BORDER_COLOR, true));
template.addProperty(Property.create(ImageLayer.PROPERTY_NAME_BORDER_WIDTH, Double.class, ImageLayer.DEFAULT_BORDER_WIDTH, true));
template.addProperty(Property.create(ImageLayer.PROPERTY_NAME_PIXEL_BORDER_SHOWN, Boolean.class, ImageLayer.DEFAULT_PIXEL_BORDER_SHOWN, true));
template.addProperty(Property.create(ImageLayer.PROPERTY_NAME_PIXEL_BORDER_COLOR, Color.class, ImageLayer.DEFAULT_PIXEL_BORDER_COLOR, true));
template.addProperty(Property.create(ImageLayer.PROPERTY_NAME_PIXEL_BORDER_WIDTH, Double.class, ImageLayer.DEFAULT_PIXEL_BORDER_WIDTH, true));
return template;
}
private static Property addImageToModelTransformModel(PropertyContainer configuration) {
Property property = configuration.getProperty(PROPERTY_NAME_IMAGE_TO_MODEL_TRANSFORM);
if (property == null) {
property = Property.create(PROPERTY_NAME_IMAGE_TO_MODEL_TRANSFORM, AffineTransform.class);
configuration.addProperty(property);
}
property.getDescriptor().setTransient(true);
return property;
}
private static Property addMultiLevelSourceModel(PropertyContainer configuration) {
if (configuration.getProperty(PROPERTY_NAME_MULTI_LEVEL_SOURCE) == null) {
configuration.addProperty(Property.create(PROPERTY_NAME_MULTI_LEVEL_SOURCE, MultiLevelSource.class));
}
configuration.getDescriptor(PROPERTY_NAME_MULTI_LEVEL_SOURCE).setTransient(true);
return configuration.getProperty(PROPERTY_NAME_MULTI_LEVEL_SOURCE);
}
}
}