/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008 - 2010, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.display2d.canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.geotoolkit.display.container.GraphicContainer;
import org.geotoolkit.display.primitive.SceneNode;
import org.geotoolkit.display2d.GO2Hints;
import org.geotoolkit.display2d.GO2Utilities;
import org.geotoolkit.display2d.canvas.painter.SolidColorPainter;
import org.geotoolkit.display2d.container.stateless.StatelessMapItemJ2D;
import org.geotoolkit.display2d.primitive.GraphicJ2D;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.image.color.ColorUtilities;
import org.geotoolkit.map.GraphicBuilder;
import org.geotoolkit.map.MapItem;
import org.geotoolkit.map.MapLayer;
import org.geotoolkit.style.MutableStyle;
import org.geotoolkit.style.visitor.ListingColorVisitor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* Canvas based on a BufferedImage.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class J2DCanvasBuffered extends J2DCanvas{
private BufferedImage buffer;
public J2DCanvasBuffered(final CoordinateReferenceSystem crs, final Dimension dim){
this(crs,dim,null);
}
public J2DCanvasBuffered(final CoordinateReferenceSystem crs, final Dimension dim, final Hints hints){
super(crs,hints);
setSize(dim);
}
public Dimension getSize(){
return getDisplayBounds().getBounds().getSize();
}
public void setSize(final Dimension dim){
setDisplayBounds(new Rectangle(dim));
}
@Override
public void setDisplayBounds(final Rectangle2D rect) {
super.setDisplayBounds(rect);
buffer = null; //todo should check if the size is really different
}
/**
* {@inheritDoc }
*/
@Override
public void dispose(){
super.dispose();
buffer = null;
}
/**
* {@inheritDoc }
*/
@Override
public BufferedImage getSnapShot(){
return buffer;
}
@Override
public void repaint(final Shape displayArea) {
//finish any previous painting
getMonitor().stopRendering();
final Dimension dim = getSize();
if(buffer == null){
//create the buffer at the last possible moment
buffer = createBufferedImage(dim);
}else{
//we clear the buffer if it exists
final Graphics2D g2D = (Graphics2D) buffer.getGraphics();
g2D.setComposite(GO2Utilities.ALPHA_COMPOSITE_0F);
g2D.fillRect(0,0,dim.width,dim.height);
}
monitor.renderingStarted();
fireRenderingStateChanged(RENDERING);
final Graphics2D output = (Graphics2D) buffer.getGraphics();
Rectangle clipBounds = output.getClipBounds();
/*
* Sets a flag for avoiding some "refresh()" events while we are actually painting.
* For example some implementation of the GraphicPrimitive2D.paint(...) method may
* detects changes since the last rendering and invokes some kind of invalidate(...)
* methods before the graphic rendering begin. Invoking those methods may cause in some
* indirect way a call to GraphicPrimitive2D.refresh(), which will trig an other widget
* repaint. This second repaint is usually not needed, since Graphics usually managed
* to update their informations before they start their rendering. Consequently,
* disabling repaint events while we are painting help to reduces duplicated rendering.
*/
if (clipBounds == null) {
clipBounds = new Rectangle(dim);
}
output.setClip(clipBounds);
output.addRenderingHints(getHints(true));
final RenderingContext2D context = prepareContext(context2D, output,null);
//paint background if there is one.
if(painter != null){
painter.paint(context2D);
}
final GraphicContainer container = getContainer();
if(container != null){
render(context, container.flatten(true));
}
/**
* End painting, erase dirtyArea
*/
output.dispose();
fireRenderingStateChanged(ON_HOLD);
monitor.renderingFinished();
}
/**
* This will try to create the most efficient bufferedImage knowing
* the different rendering parameters and hints.
* @return
*/
private BufferedImage createBufferedImage(final Dimension dim){
//See if a color model has been set, if so use it.
final ColorModel cm = (ColorModel)getRenderingHint(GO2Hints.KEY_COLOR_MODEL);
if(cm != null){
return new BufferedImage(cm,
cm.createCompatibleWritableRaster(dim.width, dim.height),
cm.isAlphaPremultiplied(), null);
}
//Get the Anti-aliasing value;
final boolean AA;
final Object val = getRenderingHint(RenderingHints.KEY_ANTIALIASING);
if(RenderingHints.VALUE_ANTIALIAS_ON == val){
AA = true;
}else{
//force AA off, to replace default value.
setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
AA = false;
}
//check background painter.
if(painter == null){
if(AA){
//Anti-aliasing enable, unpredictable colors
return new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
}
//check graphic object and see if we can predict colors
final Set<Integer> colors = extractColors(getContainer().flatten(true));
if(colors != null){
//translucent background
colors.add(0);
//we succesfully predicted the colors, makes an index color model
return createBufferedImage(dim,colors);
}else{
//we can't use a index color model, use an ARGB palette
return new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
}
}else{
if(painter.isOpaque()){
//background is opaque, we are sure we don't need an alpha
//see of we can determinate the background color, only if there is no AA.
if(!AA && painter instanceof SolidColorPainter){
//check graphic object and see if we can predict colors
final Set<Integer> colors = extractColors(getContainer().flatten(true));
if(colors != null){
final Color bgColor = ((SolidColorPainter)painter).getColor();
colors.add(bgColor.getRGB());
//we succesfully predicted the colors, makes an index color model
return createBufferedImage(dim,colors);
}else{
//we can't use a index color model, use an RGB palette
return new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
}
}else{
//we can't determinate the background colors, use an RGB palette
return new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
}
}else{
//we can't determinate the background colors, use an ARGB palette
return new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
}
}
}
private static BufferedImage createBufferedImage(final Dimension dim, final Set<Integer> colors){
if(colors.size() <= 1){
//in case no colors where used after all filters.
//icm must have at least 2 values
colors.add(Color.BLACK.getRGB());
if(colors.size() == 1){
//we are out of luck, it was black the single color.
colors.add(0); //add translucent
}
}
final int[] cmap = new int[colors.size()];
int i = 0;
for (Integer color : colors) {
cmap[i++] = color;
}
final IndexColorModel icm = ColorUtilities.getIndexColorModel(cmap);
return new BufferedImage(icm, icm.createCompatibleWritableRaster(dim.width, dim.height), icm.isAlphaPremultiplied(), null);
}
/**
* @param graphics graphics to explore
* @return Set of colors used by the graphics or null if unpredictable.
*/
private static SortedSet<Integer> extractColors(final List<SceneNode> graphics){
SortedSet<Integer> colors = new TreeSet<>(new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
//place 0 always first
if(o1 == 0){
return -1;
}if(o2 == 0){
return +1;
}else{
return o1.compareTo(o2);
}
}
});
for(SceneNode gra : graphics){
if(!(gra instanceof GraphicJ2D)){
//this node has no visual representation
continue;
}
if(gra instanceof StatelessMapItemJ2D){
final StatelessMapItemJ2D cn = (StatelessMapItemJ2D) gra;
colors = extractColors(cn.getUserObject(), colors);
}else{
//can not extract colors
return null;
}
}
return colors;
}
private static SortedSet<Integer> extractColors(final MapItem context, SortedSet<Integer> buffer){
for(MapItem child : context.items()){
if(child instanceof MapLayer){
buffer = extractColors((MapLayer)child, buffer);
}else{
buffer = extractColors(child, buffer);
}
if(buffer == null){
//unpredictable colors
return buffer;
}
}
return buffer;
}
private static SortedSet<Integer> extractColors(final MapLayer layer, final SortedSet<Integer> buffer){
final GraphicBuilder customBuilder = layer.getGraphicBuilder(GraphicJ2D.class);
if(customBuilder != null){
//this layer has a custom graphic builder, colors are unpredictable.
return null;
}
final MutableStyle style = layer.getStyle();
final ListingColorVisitor visitor = new ListingColorVisitor();
style.accept(visitor, null);
final Set<Integer> colors = visitor.getColors();
if(colors == null){
//unpredictable colors
return null;
}else{
buffer.addAll(colors);
return buffer;
}
}
}