/*
* 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.swing;
import com.bc.ceres.core.Assert;
import com.bc.ceres.glayer.CollectionLayer;
import com.bc.ceres.glayer.Layer;
import com.bc.ceres.glayer.LayerFilter;
import com.bc.ceres.glayer.support.ImageLayer;
import com.bc.ceres.glayer.support.LayerViewInvalidationListener;
import com.bc.ceres.glayer.swing.NavControl.NavControlModel;
import com.bc.ceres.grender.AdjustableView;
import com.bc.ceres.grender.InteractiveRendering;
import com.bc.ceres.grender.Rendering;
import com.bc.ceres.grender.Viewport;
import com.bc.ceres.grender.support.DefaultViewport;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
/**
* A Swing component capable of drawing a collection of {@link com.bc.ceres.glayer.Layer}s.
*
* @author Norman Fomferra
*/
public class LayerCanvas extends JPanel implements AdjustableView {
private static final boolean DEBUG = Boolean.getBoolean("ceres.renderer.debug");
private LayerCanvasModel model;
private CanvasRendering canvasRendering;
private boolean navControlShown;
private WakefulComponent navControlWrapper;
private boolean initiallyZoomingAll;
private boolean zoomedAll;
// AdjustableView properties
private Rectangle2D maxVisibleModelBounds;
private double minZoomFactor;
private double maxZoomFactor;
private double defaultZoomFactor;
private ArrayList<Overlay> overlays;
private final ModelChangeHandler modelChangeHandler;
private boolean antialiasing;
private LayerFilter layerFilter;
public LayerCanvas() {
this(new CollectionLayer());
}
public LayerCanvas(Layer layer) {
this(layer, new DefaultViewport(true));
}
public LayerCanvas(final Layer layer, final Viewport viewport) {
this(new DefaultLayerCanvasModel(layer, viewport));
}
public LayerCanvas(LayerCanvasModel model) {
super(null);
Assert.notNull(model, "model");
setOpaque(true);
this.modelChangeHandler = new ModelChangeHandler();
this.model = model;
this.model.addChangeListener(modelChangeHandler);
this.canvasRendering = new CanvasRendering();
this.overlays = new ArrayList<Overlay>(4);
this.initiallyZoomingAll = true;
this.zoomedAll = false;
this.antialiasing = true;
setNavControlShown(false);
if (!model.getViewport().getViewBounds().isEmpty()) {
setBounds(model.getViewport().getViewBounds());
}
}
public LayerCanvasModel getModel() {
return model;
}
public void setModel(LayerCanvasModel newModel) {
Assert.notNull(newModel, "newModel");
LayerCanvasModel oldModel = this.model;
if (newModel != oldModel) {
oldModel.removeChangeListener(modelChangeHandler);
zoomedAll = false;
this.model = newModel;
if (!getBounds().isEmpty()) {
this.model.getViewport().setViewBounds(getBounds());
}
updateAdjustableViewProperties();
this.model.addChangeListener(modelChangeHandler);
firePropertyChange("model", oldModel, newModel);
repaint();
}
}
public Layer getLayer() {
return model.getLayer();
}
public LayerFilter getLayerFilter() {
return layerFilter;
}
public void setLayerFilter(LayerFilter layerFilter) {
LayerFilter oldLayerFilter = this.layerFilter;
if (oldLayerFilter != layerFilter) {
this.layerFilter = layerFilter;
repaint();
firePropertyChange("layerFilter", oldLayerFilter, layerFilter);
}
}
public void dispose() {
if (model != null) {
model.removeChangeListener(modelChangeHandler);
}
}
/**
* Adds an overlay to the canvas.
*
* @param overlay An overlay
*/
public void addOverlay(Overlay overlay) {
overlays.add(overlay);
repaint();
}
/**
* Removes an overlay from the canvas.
*
* @param overlay An overlay
*/
public void removeOverlay(Overlay overlay) {
overlays.remove(overlay);
repaint();
}
/**
* None API. Don't use this method!
*
* @return true, if this canvas uses a {@link NavControl}.
*/
public boolean isNavControlShown() {
return navControlShown;
}
/**
* Checks if anti-aliased vector graphics are enabled.
* @return true, if enabled.
*/
public boolean isAntialiasing() {
return antialiasing;
}
/**
* Enables / disables anti-aliased vector graphics.
* @param antialiasing true, if enabled.
*/
public void setAntialiasing(boolean antialiasing) {
boolean oldValue = this.antialiasing;
if (oldValue != antialiasing) {
this.antialiasing = antialiasing;
firePropertyChange("antialiasing", oldValue, antialiasing);
repaint();
}
}
/**
* None API. Don't use this method!
*
* @param navControlShown true, if this canvas uses a {@link NavControl}.
*/
public void setNavControlShown(boolean navControlShown) {
boolean oldValue = this.navControlShown;
if (oldValue != navControlShown) {
if (navControlShown) {
final NavControl navControl = new NavControl(new NavControlModelImpl(getViewport()));
navControlWrapper = new WakefulComponent(navControl);
add(navControlWrapper);
} else {
remove(navControlWrapper);
navControlWrapper = null;
}
validate();
this.navControlShown = navControlShown;
}
}
public boolean isInitiallyZoomingAll() {
return initiallyZoomingAll;
}
public void setInitiallyZoomingAll(boolean initiallyZoomingAll) {
this.initiallyZoomingAll = initiallyZoomingAll;
}
public void zoomAll() {
getViewport().zoom(getMaxVisibleModelBounds());
}
/////////////////////////////////////////////////////////////////////////
// AdjustableView implementation
@Override
public Viewport getViewport() {
return model.getViewport();
}
@Override
public Rectangle2D getMaxVisibleModelBounds() {
return maxVisibleModelBounds;
}
@Override
public double getMinZoomFactor() {
return minZoomFactor;
}
@Override
public double getMaxZoomFactor() {
return maxZoomFactor;
}
@Override
public double getDefaultZoomFactor() {
return defaultZoomFactor;
}
private void updateAdjustableViewProperties() {
maxVisibleModelBounds = computeMaxVisibleModelBounds(getLayer().getModelBounds(),
getViewport().getOrientation());
minZoomFactor = computeMinZoomFactor(getViewport().getViewBounds(), maxVisibleModelBounds);
Layer layer = getLayer();
double minScale = computeMinImageToModelScale(layer);
if (minScale > 0.0) {
defaultZoomFactor = 1.0 / minScale;
maxZoomFactor = 32.0 / minScale; // empiric!
} else {
defaultZoomFactor = minZoomFactor;
maxZoomFactor = 1000.0 * minZoomFactor;
}
if (DEBUG) {
System.out.println("LayerCanvas.updateAdjustableViewProperties():");
System.out.println(" zoomFactor = " + getViewport().getZoomFactor());
System.out.println(" minZoomFactor = " + minZoomFactor);
System.out.println(" maxZoomFactor = " + maxZoomFactor);
System.out.println(" defaultZoomFactor = " + defaultZoomFactor);
System.out.println(" maxVisibleModelBounds = " + maxVisibleModelBounds);
}
}
static double computeMinZoomFactor(Rectangle2D viewBounds, Rectangle2D maxVisibleModelBounds) {
double vw = viewBounds.getWidth();
double vh = viewBounds.getHeight();
double mw = maxVisibleModelBounds.getWidth();
double mh = maxVisibleModelBounds.getHeight();
double sw = mw > 0.0 ? vw / mw : 0.0;
double sh = mh > 0.0 ? vh / mh : 0.0;
double s;
if (sw > 0.0 && sh > 0.0) {
s = Math.min(sw, sh);
} else if (sw > 0.0) {
s = sw;
} else if (sh > 0.0) {
s = sh;
} else {
s = 0.0;
}
return 0.5 * s;
}
static double computeMinImageToModelScale(Layer layer) {
return computeMinImageToModelScale(layer, 0.0);
}
private static double computeMinImageToModelScale(Layer layer, double minScale) {
if (layer instanceof ImageLayer) {
ImageLayer imageLayer = (ImageLayer) layer;
if (imageLayer.getModelBounds() != null) {
AffineTransform i2m = imageLayer.getImageToModelTransform();
double scale = Math.sqrt(Math.abs(i2m.getDeterminant()));
if (scale > 0.0 && (minScale <= 0.0 || scale < minScale)) {
minScale = scale;
}
}
}
for (Layer childLayer : layer.getChildren()) {
minScale = computeMinImageToModelScale(childLayer, minScale);
}
return minScale;
}
public static Rectangle2D computeMaxVisibleModelBounds(Rectangle2D modelBounds, double orientation) {
if (modelBounds == null) {
return new Rectangle();
}
if (orientation == 0.0) {
return modelBounds;
} else {
final AffineTransform t = new AffineTransform();
t.rotate(orientation, modelBounds.getCenterX(), modelBounds.getCenterY());
return t.createTransformedShape(modelBounds).getBounds2D();
}
}
// AdjustableView implementation
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
// JComponent overrides
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
getViewport().setViewBounds(getBounds());
}
@Override
public void doLayout() {
if (navControlShown && navControlWrapper != null) {
// Use the following code to align the nav. control to the RIGHT (nf, 18.09,.2008)
// navControlWrapper.setLocation(getWidth() - navControlWrapper.getWidth() - 4, 4);
navControlWrapper.setLocation(4, 4);
}
}
@Override
protected void paintComponent(Graphics g) {
long t0 = DEBUG ? System.nanoTime() : 0L;
if (initiallyZoomingAll && !zoomedAll && maxVisibleModelBounds != null && !maxVisibleModelBounds.isEmpty()) {
zoomedAll = true;
zoomAll();
}
final Graphics2D g2d = (Graphics2D) g;
final Object antiAliasing = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
final Object textAntiAliasing = g2d.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
if (antialiasing) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
} else {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
}
try {
super.paintComponent(g);
canvasRendering.setGraphics2D((Graphics2D) g);
getLayer().render(canvasRendering, layerFilter);
if (!isPaintingForPrint()) {
for (Overlay overlay : overlays) {
overlay.paintOverlay(this, canvasRendering);
}
}
} finally {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antiAliasing);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntiAliasing);
}
if (DEBUG) {
double dt = (System.nanoTime() - t0) / (1000.0 * 1000.0);
System.out.println("LayerCanvas.paintComponent() took " + dt + " ms");
}
}
// JComponent overrides
/////////////////////////////////////////////////////////////////////////
private class CanvasRendering implements InteractiveRendering {
private Graphics2D graphics2D;
public CanvasRendering() {
}
@Override
public Graphics2D getGraphics() {
return graphics2D;
}
void setGraphics2D(Graphics2D graphics2D) {
this.graphics2D = graphics2D;
}
@Override
public Viewport getViewport() {
return getModel().getViewport();
}
@Override
public void invalidateRegion(Rectangle region) {
repaint(region.x, region.y, region.width, region.height);
}
@Override
public void invokeLater(Runnable runnable) {
SwingUtilities.invokeLater(runnable);
}
}
private static class NavControlModelImpl implements NavControlModel {
private final Viewport viewport;
public NavControlModelImpl(Viewport viewport) {
this.viewport = viewport;
}
@Override
public double getCurrentAngle() {
return Math.toDegrees(viewport.getOrientation());
}
@Override
public void handleRotate(double rotationAngle) {
viewport.setOrientation(Math.toRadians(rotationAngle));
}
@Override
public void handleMove(double moveDirX, double moveDirY) {
viewport.moveViewDelta(16 * moveDirX, 16 * moveDirY);
}
@Override
public void handleScale(double scaleDir) {
final double oldZoomFactor = viewport.getZoomFactor();
final double newZoomFactor = (1.0 + 0.1 * scaleDir) * oldZoomFactor;
viewport.setZoomFactor(newZoomFactor);
}
}
public interface Overlay {
void paintOverlay(LayerCanvas canvas, Rendering rendering);
}
private class ModelChangeHandler extends LayerViewInvalidationListener implements LayerCanvasModel.ChangeListener {
@Override
public void handleViewInvalidation(Layer layer, Rectangle2D modelRegion) {
updateAdjustableViewProperties();
if (modelRegion != null) {
AffineTransform m2v = getViewport().getModelToViewTransform();
Rectangle viewRegion = m2v.createTransformedShape(modelRegion).getBounds();
repaint(viewRegion);
} else {
repaint();
}
}
@Override
public void handleViewportChanged(Viewport viewport, boolean orientationChanged) {
updateAdjustableViewProperties();
repaint();
}
}
}