/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.client.ui.zone;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import org.apache.log4j.Logger;
import com.t3.CodeTimer;
import com.t3.image.ImageUtil;
import com.t3.model.drawing.Drawable;
import com.t3.model.drawing.DrawnElement;
import com.t3.model.drawing.Pen;
/**
*/
public class PartitionedDrawableRenderer implements DrawableRenderer {
private static Logger log = Logger.getLogger(PartitionedDrawableRenderer.class);
private static boolean messageLogged = false;
private static final int CHUNK_SIZE = 256;
private static List<BufferedImage> unusedChunkList = new LinkedList<BufferedImage>();
private final Set<String> noImageSet = new HashSet<String>();
private final List<Tuple> chunkList = new LinkedList<Tuple>();
private int maxChunks;
private double lastDrawableCount;
private double lastScale;
private Rectangle lastViewport;
private int horizontalChunkCount;
private int verticalChunkCount;
private CodeTimer timer;
@Override
public void flush() {
int unusedSize = unusedChunkList.size();
for (Tuple tuple : chunkList) {
// Reuse the images
if (unusedSize < maxChunks && tuple != null) {
unusedChunkList.add(tuple.image);
unusedSize++;
}
}
chunkList.clear();
noImageSet.clear();
}
@Override
public void renderDrawables(Graphics g, List<DrawnElement> drawableList, Rectangle viewport, double scale) {
timer = new CodeTimer("Renderer");
timer.setThreshold(10);
timer.setEnabled(false);
// NOTHING TO DO
if (drawableList == null || drawableList.size() == 0) {
flush();
return;
}
// View changed ?
if (drawableList.size() != lastDrawableCount || lastScale != scale) {
flush();
}
if (lastViewport == null || viewport.width != lastViewport.width || viewport.height != lastViewport.height) {
horizontalChunkCount = (int) Math.ceil(viewport.width / (double) CHUNK_SIZE) + 1;
verticalChunkCount = (int) Math.ceil(viewport.height / (double) CHUNK_SIZE) + 1;
maxChunks = (horizontalChunkCount * verticalChunkCount * 2);
}
// Compute grid
int gridx = (int) Math.floor(-viewport.x / (double) CHUNK_SIZE);
int gridy = (int) Math.floor(-viewport.y / (double) CHUNK_SIZE);
// OK, weirdest hack ever. Basically, when the viewport.x is exactly divisible by the chunk size, the gridx decrements
// too early, creating a visual jump in the drawables. I don't know the exact cause, but this seems to account for it
// note that it only happens in the negative space. Weird.
gridx += (viewport.x > CHUNK_SIZE && (viewport.x % CHUNK_SIZE == 0) ? -1 : 0);
gridy += (viewport.y > CHUNK_SIZE && (viewport.y % CHUNK_SIZE == 0) ? -1 : 0);
for (int row = 0; row < verticalChunkCount; row++) {
for (int col = 0; col < horizontalChunkCount; col++) {
int cellX = gridx + col;
int cellY = gridy + row;
String key = getKey(cellX, cellY);
if (noImageSet.contains(key)) {
continue;
}
Tuple chunk = findChunk(chunkList, key);
if (chunk == null) {
chunk = new Tuple(key, createChunk(drawableList, cellX, cellY, scale));
if (chunk.image == null) {
noImageSet.add(key);
continue;
}
}
// Most recently used is at the front
chunkList.add(0, chunk);
// Trim to the right size
if (chunkList.size() > maxChunks) {
int chunkSize = chunkList.size();
// chunkList.subList(maxChunks, chunkSize).clear();
while (chunkSize > maxChunks) {
chunkList.remove(--chunkSize);
}
}
int x = col * CHUNK_SIZE - ((CHUNK_SIZE - viewport.x)) % CHUNK_SIZE - (gridx < -1 ? CHUNK_SIZE : 0);
int y = row * CHUNK_SIZE - ((CHUNK_SIZE - viewport.y)) % CHUNK_SIZE - (gridy < -1 ? CHUNK_SIZE : 0);
timer.start("render:DrawImage");
g.drawImage(chunk.image, x, y, null);
timer.stop("render:DrawImage");
// DEBUG: Partition boundaries
if (log.isDebugEnabled()) { // Show partition boundaries
if (!messageLogged) {
messageLogged = true;
log.debug("DEBUG logging of " + this.getClass().getSimpleName() + " causes colored rectangles and message strings.");
}
if (col % 2 == 0) {
if (row % 2 == 0) {
g.setColor(Color.white);
} else {
g.setColor(Color.green);
}
} else {
if (row % 2 == 0) {
g.setColor(Color.green);
} else {
g.setColor(Color.white);
}
}
g.drawRect(x, y, CHUNK_SIZE - 1, CHUNK_SIZE - 1);
g.drawString(key, x + CHUNK_SIZE / 2, y + CHUNK_SIZE / 2);
}
}
}
// REMEMBER
lastViewport = viewport;
lastDrawableCount = drawableList.size();
lastScale = scale;
if (timer.isEnabled()) {
// System.out.println(timer);
}
}
/**
* Given a List and a String key, find the element in the list that matches
* the key.
*
* @param list
* @param key
* @return
*/
private Tuple findChunk(List<Tuple> list, String key) {
ListIterator<Tuple> iter = list.listIterator();
while (iter.hasNext()) {
Tuple tuple = iter.next();
if (tuple.equals(key)) {
iter.remove();
return tuple;
}
}
return null;
}
private BufferedImage createChunk(List<DrawnElement> drawableList, int gridx, int gridy, double scale) {
int x = gridx * CHUNK_SIZE;
int y = gridy * CHUNK_SIZE;
BufferedImage image = null;
Composite oldComposite = null;
Graphics2D g = null;
for (DrawnElement element : drawableList) {
timer.start("createChunk:calculate");
Drawable drawable = element.getDrawable();
Rectangle2D drawnBounds = new Rectangle(drawable.getBounds());
Rectangle2D chunkBounds = new Rectangle((int) (gridx * (CHUNK_SIZE / scale)), (int) (gridy * (CHUNK_SIZE / scale)), (int) (CHUNK_SIZE / scale), (int) (CHUNK_SIZE / scale));
// Handle pen size
Pen pen = element.getPen();
int penSize = (int) (pen.getThickness() / 2 + 1);
drawnBounds.setRect(drawnBounds.getX() - penSize, drawnBounds.getY() - penSize, drawnBounds.getWidth() + pen.getThickness(), drawnBounds.getHeight() + pen.getThickness());
timer.stop("createChunk:calculate");
timer.start("createChunk:BoundsCheck");
if (!drawnBounds.intersects(chunkBounds)) {
timer.stop("createChunk:BoundsCheck");
continue;
}
timer.stop("createChunk:BoundsCheck");
timer.start("createChunk:CreateChunk");
if (image == null) {
image = getNewChunk();
g = image.createGraphics();
g.setClip(0, 0, CHUNK_SIZE, CHUNK_SIZE);
oldComposite = g.getComposite();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
AffineTransform af = new AffineTransform();
af.translate(-x, -y);
af.scale(scale, scale);
g.setTransform(af);
}
timer.stop("createChunk:CreateChunk");
if (pen.getOpacity() != 1 && pen.getOpacity() != 0 /*
* handle legacy
* pens,
* besides, it
* doesn't make
* sense to have
* a non visible
* pen
*/) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pen.getOpacity()));
}
// g.setColor(Color.red);
// g.draw(drawnBounds);
timer.start("createChunk:Draw");
drawable.draw(g, pen);
g.setComposite(oldComposite);
timer.stop("createChunk:Draw");
}
if (g != null) {
g.dispose();
}
return image;
}
private BufferedImage getNewChunk() {
BufferedImage image = null;
if (unusedChunkList.size() > 0) {
image = unusedChunkList.remove(0);
ImageUtil.clearImage(image);
} else {
image = new BufferedImage(CHUNK_SIZE, CHUNK_SIZE, Transparency.BITMASK);
}
image.setAccelerationPriority(1);
return image;
}
private String getKey(int col, int row) {
return col + "." + row;
}
private static class Tuple {
String key;
BufferedImage image;
public Tuple(String key, BufferedImage image) {
this.key = key;
this.image = image;
}
@Override
public boolean equals(Object obj) {
return key.equals(obj.toString());
}
}
}