/*
* 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.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
import com.t3.client.AppUtil;
import com.t3.model.drawing.Drawable;
import com.t3.model.drawing.DrawnElement;
import com.t3.model.drawing.Pen;
import com.t3.persistence.FileUtil;
/**
*/
public class DiskBasedPartitionedDrawableRenderer implements DrawableRenderer {
private static final File CACHE_DIR = AppUtil.getAppHome("dbpdrcache");
private static final BufferedImage NO_IMAGE = new BufferedImage(1, 1, Transparency.OPAQUE);
private static final int CHUNK_SIZE = 256;
private final Map<String, BufferedImage> chunkMap = new HashMap<String, BufferedImage>();
private double lastDrawableCount;
private double lastScale;
private Rectangle lastViewport;
private int horizontalChunkCount;
private int verticalChunkCount;
static {
try {
// Clear out the cache. Boot up is as good of a time as any
if (CACHE_DIR.exists()) {
FileUtil.delete(CACHE_DIR);
}
CACHE_DIR.mkdirs();
} catch (Exception ioe) {
ioe.printStackTrace();
}
}
public DiskBasedPartitionedDrawableRenderer() {
flush();
}
@Override
public void flush() {
File chunkDir = getChunkDir();
try {
if (chunkDir.exists()) {
FileUtil.delete(chunkDir);
}
chunkDir.mkdirs();
} catch (Exception ioe) {
ioe.printStackTrace();
}
chunkMap.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();
}
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;
}
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());
int count = 0;
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) {
chunk = createChunk(drawableList, cellX, cellY, scale);
chunkMap.put(key, chunk);
}
if (chunk != null && chunk != NO_IMAGE) {
// System.out.println("Drawing: " + key);
count++;
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) {
// Keep NO_IMAGEs in memory since they take virtually no space, and reduces the amount of work to do
if (chunkMap.get(key) != NO_IMAGE) {
chunkMap.remove(key);
}
}
// System.out.println("Chunks: " + count);
// REMEMBER
lastViewport = viewport;
lastDrawableCount = drawableList.size();
lastScale = scale;
}
private BufferedImage createChunk(List<DrawnElement> drawableList, int gridx, int gridy, double scale) {
// Have we already cached it ?
File chunkFile = getChunkFile(gridx, gridy);
if (chunkFile.exists()) {
try {
BufferedImage image = ImageIO.read(chunkFile);
// System.out.println("Using cache: " + gridx + ", " + gridy);
return image;
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
// System.out.println("Creating " + gridx + ", " + gridy);
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();
// Handle legacy pens; besides, it doesn't make sense to have a non-visible pen.
if (pen.getOpacity() != 1 && pen.getOpacity() != 0) {
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;
} else {
try {
ImageIO.write(image, "png", chunkFile);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
return image;
}
private File getChunkDir() {
return new File(CACHE_DIR.getAbsolutePath() + "/" + hashCode());
}
private File getChunkFile(int x, int y) {
return new File(getChunkDir().getAbsolutePath() + "/" + x + "-" + y);
}
private BufferedImage getNewChunk() {
return new BufferedImage(CHUNK_SIZE, CHUNK_SIZE, Transparency.BITMASK);
}
private String getKey(int col, int row) {
return col + "." + row;
}
}