/*
* 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.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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import com.t3.client.TabletopTool;
import com.t3.model.drawing.Drawable;
import com.t3.model.drawing.DrawnElement;
import com.t3.model.drawing.Pen;
/**
*/
public class ASyncPartitionedDrawableRenderer implements DrawableRenderer {
private static final BufferedImage NO_IMAGE = new BufferedImage(1, 1, Transparency.OPAQUE);
private static final int CHUNK_SIZE = 256;
private Map<String, BufferedImage> chunkMap = new HashMap<String, BufferedImage>();
private static BlockingQueue<QueuedRenderer> renderQueue = new LinkedBlockingQueue<QueuedRenderer>();
private double lastDrawableCount;
private double lastScale;
private Rectangle lastViewport;
private int horizontalChunkCount;
private int verticalChunkCount;
static {
new RenderThread().start();
}
@Override
public void flush() {
// for (BufferedImage image : chunkMap.values()) {
// releaseChunk(image);
// }
// chunkMap.clear();
renderQueue.clear();
}
@Override
public void renderDrawables(Graphics g, List<DrawnElement> drawableList, Rectangle viewport, double scale) {
// NOTHING TO DO
if (drawableList == null || drawableList.size() == 0) {
flush();
return;
}
if (drawableList.size() != lastDrawableCount || lastScale != scale) {
flush();
}
boolean forceRedraw = true;
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;
forceRedraw = true;
}
forceRedraw = lastScale != scale || !lastViewport.equals(viewport);
// REMEMBER
lastViewport = viewport;
lastDrawableCount = drawableList.size();
lastScale = scale;
// Calculate cells
int gridx = (int)Math.floor(-viewport.x / (double)CHUNK_SIZE);
int gridy = (int)Math.floor(-viewport.y / (double)CHUNK_SIZE);
Set<String> chunkCache = new HashSet<String>();
chunkCache.addAll(chunkMap.keySet());
for (int row = 0; row < verticalChunkCount; row++) {
for (int col = 0; col < horizontalChunkCount; col++) {
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);
int cellX = gridx + col;
int cellY = gridy + row;
String key = getKey(cellX, cellY);
BufferedImage chunk = chunkMap.get(key);
if (chunk == null || forceRedraw) {
createChunk(drawableList, cellX, cellY, scale, viewport);
if (chunk == null) chunk = NO_IMAGE;
chunkMap.put(key, chunk);
} else {
// System.out.println("cache: " + getKey(cellX, cellY) + " - " + (chunk == NO_IMAGE));
}
if (chunk != null && chunk != NO_IMAGE) {
// System.out.println("Drawing: " + key);
g.drawImage(chunk, x, y, null);
}
chunkCache.remove(key);
// 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);
}
}
for (String key : chunkCache) {
// System.out.println("Removing: " + key);
releaseChunk(chunkMap.remove(key));
}
}
private void createChunk(List<DrawnElement> drawableList, int gridx, int gridy, double scale, Rectangle view) {
// System.out.println("create: " + getKey(gridx, gridy));
renderQueue.add(new QueuedRenderer(drawableList, gridx, gridy, scale, view));
}
private static BufferedImage renderChunk(QueuedRenderer renderer) {
int gridx = renderer.cellX;
int gridy = renderer.cellY;
List<DrawnElement> drawableList = renderer.drawableList;
double scale = renderer.scale;
int x = gridx * CHUNK_SIZE;
int y = gridy * CHUNK_SIZE;
BufferedImage image = null;
Composite oldComposite = null;
Graphics2D g = null;
for (DrawnElement element : drawableList) {
Drawable drawable = element.getDrawable();
Rectangle2D drawnBounds = drawable.getBounds();
Rectangle2D chunkBounds = new Rectangle((int)(gridx * (CHUNK_SIZE/scale)), (int)(gridy * (CHUNK_SIZE/scale)), (int)(CHUNK_SIZE/scale), (int)(CHUNK_SIZE/scale));
// if (gridx == 0 && gridy == 1) {
// System.out.println(drawnBounds.intersects(chunkBounds));
// System.out.println(drawnBounds + " - " + chunkBounds);
// }
// TODO: handle pen size
if (!drawnBounds.intersects(chunkBounds)) {
continue;
}
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);
}
Pen pen = element.getPen();
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()));
}
// if (gridx == 0 && gridy == 1) {
// System.out.println("draw");
// }
drawable.draw(g, pen);
g.setComposite(oldComposite);
}
if (g != null) {
g.dispose();
}
// if (image != null && isEmpty(image)) {
// releaseChunk(image);
// image = null;
// }
if (image == null) {
image = NO_IMAGE;
}
return image;
}
private static void releaseChunk(BufferedImage image) {
// if (image == null || image == NO_IMAGE || chunkPool.size() >= maxChunkPoolSize) {
// return;
// }
// clearImage(image);
// chunkPool.add(image);
}
private static BufferedImage getNewChunk() {
// System.out.println("New chunk");
return new BufferedImage(CHUNK_SIZE, CHUNK_SIZE, Transparency.BITMASK);
}
private static String getKey(int col, int row) {
return col + "." + row;
}
private class QueuedRenderer {
List<DrawnElement> drawableList;
int cellX, cellY;
double scale;
Rectangle view;
public QueuedRenderer(List<DrawnElement> drawableList, int cellX, int cellY, double scale, Rectangle view) {
this.drawableList = drawableList;
this.cellX = cellX;
this.cellY = cellY;
this.scale = scale;
this.view = view;
}
public boolean isValid() {
return view.equals(lastViewport) && scale == lastScale;
}
public void render() {
// System.out.println("rendering:" + getKey(cellX, cellY));
BufferedImage chunk = renderChunk(this);
// System.out.println("putting:" + getKey(cellX, cellY) + " - " + (chunk == NO_IMAGE));
chunkMap.put(getKey(cellX, cellY), chunk);
}
}
private static class RenderThread extends Thread {
@Override
public void run() {
while (true) {
try {
QueuedRenderer renderer = renderQueue.take();
if (!renderer.isValid()) {
// System.out.println("invalid: " + getKey(renderer.cellX, renderer.cellY));
continue;
}
renderer.render();
TabletopTool.getFrame().refresh();
} catch (InterruptedException ie) {
ie.printStackTrace();
// Continue working
}
}
}
}
}