/*
* 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.image;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
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 javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import com.t3.MD5Key;
import com.t3.swing.SwingUtil;
/**
* Mechanism to view very, very large images in memory without taking up the memory.
* Works by precomputing chunks of the image at various zoom levels. These are then stored
* in a cache directory, specified during creation.
*
* Then entire image has to be in memory at some point in order to chunk it, but once it's
* been processed it can identify the cache via the image bytes (perhaps loaded from disk)
*
* @author trevor
*/
public class LargeImage {
private static final int CHUNK_SIZE = 250;
// Can scale in such that there are this many extra chunks in view
private static final int SCALE_CHUNK_INTERVAL = 4;
private double scale;
private Rectangle currentView;
private BufferedImage currentViewBuffer;
private ImageInfo info;
private File cacheDir;
private double[] scaleArray;
private Map<Integer, BufferedImage> loadedChunkMap = new HashMap<Integer, BufferedImage>();
List<Integer> usedChunkList = new ArrayList<Integer>();
public LargeImage(byte[] image, File cacheDir) {
}
public LargeImage(BufferedImage image, File cacheDir) throws IOException {
info = loadInfo();
if (info == null) {
info = new ImageInfo(image.getWidth(), image.getHeight());
}
this.cacheDir = new File(cacheDir.getAbsolutePath() + File.separator + getId(image));
this.cacheDir.mkdirs();
chunkize(image);
}
private ImageInfo loadInfo() {
return null;
}
private BufferedImage getImageView(Rectangle view, double scale) {
if (currentView != null && currentView.equals(view) && currentViewBuffer != null && this.scale == scale) {
return currentViewBuffer;
}
if (currentViewBuffer == null || currentViewBuffer.getWidth() != view.width || currentViewBuffer.getHeight() != view.height) {
currentViewBuffer = new BufferedImage(view.width, view.height, Transparency.OPAQUE);
}
Graphics2D g = currentViewBuffer.createGraphics();
// Background
g.setColor(Color.black);
g.fillRect(0, 0, view.width, view.height);
double xOffset = (view.x % CHUNK_SIZE) * scale;
double yOffset = (view.y % CHUNK_SIZE) * scale;
int visibleXChunks = (int)Math.ceil((view.width + xOffset) / (CHUNK_SIZE * scale));
int visibleYChunks = (int)Math.ceil((view.height + yOffset) / (CHUNK_SIZE * scale));
for (int row = 0; row < visibleYChunks; row++) {
for (int col = 0; col < visibleXChunks; col++) {
int chunkX = view.x / CHUNK_SIZE + col;
int chunkY = view.y / CHUNK_SIZE + row;
BufferedImage chunk = getChunk(chunkX, chunkY, scale);
if (chunk == null) {
continue;
}
int x = (int)(col * CHUNK_SIZE * scale - xOffset);
int y = (int)(row * CHUNK_SIZE * scale - yOffset);
g.drawImage(chunk, x, y, (int)Math.ceil(CHUNK_SIZE * scale), (int)Math.ceil(CHUNK_SIZE * scale), null);
}
}
flushChunkCache();
g.dispose();
return currentViewBuffer;
}
private void flushChunkCache() {
Set<Integer> keySet = new HashSet<Integer>();
keySet.addAll(loadedChunkMap.keySet());
for (Integer num : keySet) {
if (!usedChunkList.contains(num)) {
loadedChunkMap.remove(num);
}
}
usedChunkList.clear();
// System.out.println(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
}
private BufferedImage getChunk(int col, int row, double scale) {
int scaleIndex = getScaleIndex(scale);
int chunkId = getChunkId(col, row, scaleIndex);
if (col >= getChunkCountX(scaleIndex) || row >= getChunkCountY(scaleIndex) ||
col < 0 || row < 0) {
return null;
}
usedChunkList.add(chunkId);
BufferedImage chunk = loadedChunkMap.get(chunkId);
if (chunk != null) {
return chunk;
}
try {
chunk = ImageIO.read(getChunkFilename(col, row, scaleIndex));
loadedChunkMap.put(chunkId, chunk);
} catch (IOException ioe) {
ioe.printStackTrace();
}
return chunk;
}
private int getScaleIndex(double scale) {
// Pick the highest (Biggest) scale that is closest to the target scale
for (int i = 0; i < getScaleArray().length - 1; i++) {
if (scale > scaleArray[i+1]) {
return i;
}
}
return getScaleArray().length-1;
}
private double[] getScaleArray() {
if (scaleArray == null) {
scaleArray = new double[getScaleCount()];
scaleArray[0] = 1; // 0 is always 1:1
for (int i = 1; i < scaleArray.length; i++) {
scaleArray[i] = (double)(info.width - i * CHUNK_SIZE * SCALE_CHUNK_INTERVAL) / info.width;
}
}
return scaleArray;
}
private int getChunkId(int col, int row, int scaleIndex) {
return (scaleIndex * getChunkCountX(0) * getChunkCountY(0)) + row * getChunkCountX(scaleIndex) + col;
}
private int getChunkCountX(int scaleIndex) {
return (int)Math.ceil((info.width * getScaleArray()[scaleIndex]) / CHUNK_SIZE);
}
private int getChunkCountY(int scaleIndex) {
return (int)Math.ceil((info.width * getScaleArray()[scaleIndex]) / CHUNK_SIZE);
}
private int getScaleCount() {
return (int)Math.max(Math.ceil(info.width/(CHUNK_SIZE*SCALE_CHUNK_INTERVAL)), Math.ceil(info.height/(CHUNK_SIZE*SCALE_CHUNK_INTERVAL)));
}
private void chunkize(BufferedImage image) throws IOException {
for (int scaleIndex = 0; scaleIndex < getScaleCount(); scaleIndex ++) {
int chunksX = getChunkCountX(scaleIndex);
int chunksY = getChunkCountY(scaleIndex);
double scaledChunkSize = CHUNK_SIZE / getScaleArray()[scaleIndex];
BufferedImage tmpImage = new BufferedImage(CHUNK_SIZE, CHUNK_SIZE, Transparency.OPAQUE);
for (int row = 0; row < chunksY; row++) {
for (int col = 0; col < chunksX; col++) {
// Don't bother if it's already been created
File filename = getChunkFilename(col, row, scaleIndex);
if (filename.exists()) {
continue;
}
int width = (int) Math.min(scaledChunkSize, image.getWidth() - col * scaledChunkSize);
int height = (int) Math.min(scaledChunkSize, image.getHeight() - row * scaledChunkSize);
BufferedImage chunkImage = image.getSubimage((int)(col * scaledChunkSize), (int)(row * scaledChunkSize), width, height);
Graphics2D g = tmpImage.createGraphics();
g.drawImage(chunkImage, 0, 0, CHUNK_SIZE, CHUNK_SIZE, null);
g.dispose();
ImageIO.write(tmpImage, "jpg", filename);
}
}
}
}
private File getChunkFilename(int col, int row, int scaleIndex) {
return new File(cacheDir.getAbsolutePath() + File.separator + scaleIndex + "-" + col + "-" + row + ".jpg");
}
private String getId(BufferedImage image) throws IOException {
return getId(ImageUtil.imageToBytes(image));
}
private String getId(byte[] image) {
return new MD5Key(image).toString();
}
public static class LoaderFrame extends JFrame {
public LoaderFrame(LargeImage image) {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new BorderLayout());
setBounds(200, 200, 200, 200);
add(BorderLayout.CENTER, new LoaderViewPanel(image));
}
}
public static class LoaderViewPanel extends JPanel {
LargeImage image;
Rectangle view = new Rectangle();
double scale = 1;
int dragX, dragY;
public LoaderViewPanel(LargeImage image) {
this.image = image;
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
dragX = e.getX();
dragY = e.getY();
if (!SwingUtilities.isLeftMouseButton(e)) {
scale += SwingUtil.isShiftDown(e) ? .01 : -.01;
repaint();
}
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
view.x -= e.getX() - dragX;
view.y -= e.getY() - dragY;
dragX = e.getX();
dragY = e.getY();
repaint();
}
}
});
}
@Override
protected void paintComponent(Graphics g) {
Dimension size = getSize();
view.setBounds(view.x, view.y, size.width, size.height);
BufferedImage imageView = image.getImageView(view, scale);
g.drawImage(imageView, 0, 0, this);
}
}
private static class ImageInfo {
int width;
int height;
public ImageInfo(int width, int height) {
this.width = width;
this.height = height;
}
}
public static void main(String[] args) throws Exception {
ImageIO.setUseCache(false);
LargeImage image = new LargeImage(ImageIO.read(new File("map.jpg")), new File("images"));
System.out.println("START: " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
new LoaderFrame(image).setVisible(true);
}
}