diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java
index adceaead..c20d8000 100755
--- a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java
+++ b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java
@@ -158,7 +158,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException();
}
- return (short) (((byte2 << 24) >>> 16) + (byte1 << 24) >>> 24);
+ return (short) (((byte2 << 24) >>> 16) | (byte1 << 24) >>> 24);
}
/**
@@ -198,7 +198,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException();
}
- return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24));
+ return (char) (((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24));
}
@@ -221,8 +221,8 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException();
}
- return (byte4 << 24) + ((byte3 << 24) >>> 8)
- + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24);
+ return (byte4 << 24) | ((byte3 << 24) >>> 8)
+ | ((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24);
}
/**
@@ -248,10 +248,10 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da
throw new EOFException();
}
- return (byte8 << 56) + ((byte7 << 56) >>> 8)
- + ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24)
- + ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40)
- + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56);
+ return (byte8 << 56) | ((byte7 << 56) >>> 8)
+ | ((byte6 << 56) >>> 16) | ((byte5 << 56) >>> 24)
+ | ((byte4 << 56) >>> 32) | ((byte3 << 56) >>> 40)
+ | ((byte2 << 56) >>> 48) | ((byte1 << 56) >>> 56);
}
/**
diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java b/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java
new file mode 100644
index 00000000..1f7c551b
--- /dev/null
+++ b/common/common-io/src/test/java/com/twelvemonkeys/io/LittleEndianDataInputStreamTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2013, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.io;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+/**
+ * LittleEndianDataInputStreamTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: LittleEndianDataInputStreamTest.java,v 1.0 15.02.13 11:04 haraldk Exp$
+ */
+public class LittleEndianDataInputStreamTest {
+ @Test
+ public void testReadBoolean() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(new byte[] {0, 1, 0x7f, (byte) 0xff}));
+ assertFalse(data.readBoolean());
+ assertTrue(data.readBoolean());
+ assertTrue(data.readBoolean());
+ assertTrue(data.readBoolean());
+ }
+
+ @Test
+ public void testReadByte() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
+ new byte[] {
+ (byte) 0x00, (byte) 0x00,
+ (byte) 0x01, (byte) 0x00,
+ (byte) 0xff, (byte) 0xff,
+ (byte) 0x00, (byte) 0x80,
+ (byte) 0xff, (byte) 0x7f,
+ (byte) 0x00, (byte) 0x01,
+ }
+
+ ));
+
+ assertEquals(0, data.readByte());
+ assertEquals(0, data.readByte());
+ assertEquals(1, data.readByte());
+ assertEquals(0, data.readByte());
+ assertEquals(-1, data.readByte());
+ assertEquals(-1, data.readByte());
+ assertEquals(0, data.readByte());
+ assertEquals(Byte.MIN_VALUE, data.readByte());
+ assertEquals(-1, data.readByte());
+ assertEquals(Byte.MAX_VALUE, data.readByte());
+ assertEquals(0, data.readByte());
+ assertEquals(1, data.readByte());
+ }
+
+ @Test
+ public void testReadUnsignedByte() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
+ new byte[] {
+ (byte) 0x00, (byte) 0x00,
+ (byte) 0x01, (byte) 0x00,
+ (byte) 0xff, (byte) 0xff,
+ (byte) 0x00, (byte) 0x80,
+ (byte) 0xff, (byte) 0x7f,
+ (byte) 0x00, (byte) 0x01,
+ }
+
+ ));
+
+ assertEquals(0, data.readUnsignedByte());
+ assertEquals(0, data.readUnsignedByte());
+ assertEquals(1, data.readUnsignedByte());
+ assertEquals(0, data.readUnsignedByte());
+ assertEquals(255, data.readUnsignedByte());
+ assertEquals(255, data.readUnsignedByte());
+ assertEquals(0, data.readUnsignedByte());
+ assertEquals(128, data.readUnsignedByte());
+ assertEquals(255, data.readUnsignedByte());
+ assertEquals(Byte.MAX_VALUE, data.readUnsignedByte());
+ assertEquals(0, data.readUnsignedByte());
+ assertEquals(1, data.readUnsignedByte());
+ }
+
+ @Test
+ public void testReadShort() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
+ new byte[] {
+ (byte) 0x00, (byte) 0x00,
+ (byte) 0x01, (byte) 0x00,
+ (byte) 0xff, (byte) 0xff,
+ (byte) 0x00, (byte) 0x80,
+ (byte) 0xff, (byte) 0x7f,
+ (byte) 0x00, (byte) 0x01,
+ }
+
+ ));
+
+ assertEquals(0, data.readShort());
+ assertEquals(1, data.readShort());
+ assertEquals(-1, data.readShort());
+ assertEquals(Short.MIN_VALUE, data.readShort());
+ assertEquals(Short.MAX_VALUE, data.readShort());
+ assertEquals(256, data.readShort());
+ }
+
+ @Test
+ public void testReadUnsignedShort() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
+ new byte[] {
+ (byte) 0x00, (byte) 0x00,
+ (byte) 0x01, (byte) 0x00,
+ (byte) 0xff, (byte) 0xff,
+ (byte) 0x00, (byte) 0x80,
+ (byte) 0xff, (byte) 0x7f,
+ (byte) 0x00, (byte) 0x01,
+ }
+
+ ));
+
+ assertEquals(0, data.readUnsignedShort());
+ assertEquals(1, data.readUnsignedShort());
+ assertEquals(Short.MAX_VALUE * 2 + 1, data.readUnsignedShort());
+ assertEquals(Short.MAX_VALUE + 1, data.readUnsignedShort());
+ assertEquals(Short.MAX_VALUE, data.readUnsignedShort());
+ assertEquals(256, data.readUnsignedShort());
+ }
+
+ @Test
+ public void testReadInt() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
+ new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+ (byte) 0xff, (byte) 0x00, (byte) 0xff, (byte) 0x00,
+ (byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0xff,
+ (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca,
+ (byte) 0xca, (byte) 0xfe, (byte) 0xd0, (byte) 0x0d,
+ }
+
+ ));
+
+ assertEquals(0, data.readInt());
+ assertEquals(1, data.readInt());
+ assertEquals(-1, data.readInt());
+ assertEquals(Integer.MIN_VALUE, data.readInt());
+ assertEquals(Integer.MAX_VALUE, data.readInt());
+ assertEquals(16777216, data.readInt());
+ assertEquals(0xff00ff, data.readInt());
+ assertEquals(0xff00ff00, data.readInt());
+ assertEquals(0xCafeBabe, data.readInt());
+ assertEquals(0x0dd0feca, data.readInt());
+ }
+
+ @Test
+ public void testReadLong() throws IOException {
+ LittleEndianDataInputStream data = new LittleEndianDataInputStream(new ByteArrayInputStream(
+ new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+ (byte) 0x0d, (byte) 0xd0, (byte) 0xfe, (byte) 0xca, (byte) 0xbe, (byte) 0xba, (byte) 0xfe, (byte) 0xca,
+ }
+
+ ));
+
+ assertEquals(0, data.readLong());
+ assertEquals(1, data.readLong());
+ assertEquals(-1, data.readLong());
+ assertEquals(Long.MIN_VALUE, data.readLong());
+ assertEquals(Long.MAX_VALUE, data.readLong());
+ assertEquals(72057594037927936L, data.readLong());
+ assertEquals(0xCafeBabeL << 32 | 0xCafeD00dL, data.readLong());
+ }
+}
diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java
index 7a31a119..8d69ffce 100644
--- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java
+++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java
@@ -30,6 +30,7 @@ package com.twelvemonkeys.image;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.lang.StringUtil;
+import com.twelvemonkeys.util.LRUHashMap;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
@@ -38,17 +39,17 @@ import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.awt.*;
-import java.awt.geom.AffineTransform;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.io.File;
import java.io.IOException;
-import java.util.Iterator;
-import java.util.Random;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.*;
/**
* MappedBufferImage
@@ -59,7 +60,7 @@ import java.util.concurrent.TimeUnit;
*/
public class MappedBufferImage {
private static int threads = Runtime.getRuntime().availableProcessors();
- private static ExecutorService executorService = Executors.newFixedThreadPool(threads);
+ private static ExecutorService executorService = Executors.newFixedThreadPool(threads * 4);
public static void main(String[] args) throws IOException {
int argIndex = 0;
@@ -91,8 +92,9 @@ public class MappedBufferImage {
// TODO: Negotiate best layout according to the GraphicsConfiguration.
- w = reader.getWidth(0);
- h = reader.getHeight(0);
+ int sub = 1;
+ w = reader.getWidth(0) / sub;
+ h = reader.getHeight(0) / sub;
// GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
// ColorModel cm2 = configuration.getColorModel(cm.getTransparency());
@@ -111,8 +113,11 @@ public class MappedBufferImage {
System.out.println("image = " + image);
+ // TODO: Display image while reading
+
ImageReadParam param = reader.getDefaultReadParam();
param.setDestination(image);
+ param.setSourceSubsampling(sub, sub, 0, 0);
reader.addIIOReadProgressListener(new ConsoleProgressListener());
reader.read(0, param);
@@ -166,7 +171,7 @@ public class MappedBufferImage {
return size;
}
};
- frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
JScrollPane scroll = new JScrollPane(new ImageComponent(image));
scroll.setBorder(BorderFactory.createEmptyBorder());
frame.add(scroll);
@@ -184,13 +189,24 @@ public class MappedBufferImage {
// NOTE: The createCompatibleDestImage takes the byte order/layout into account, unlike the cm.createCompatibleWritableRaster
final BufferedImage output = new ResampleOp(width, height).createCompatibleDestImage(image, null);
- final int inStep = (int) Math.ceil(image.getHeight() / (double) threads);
- final int outStep = (int) Math.ceil(height / (double) threads);
+ final int steps = threads * height / 100;
+ final int inStep = (int) Math.ceil(image.getHeight() / (double) steps);
+ final int outStep = (int) Math.ceil(height / (double) steps);
- final CountDownLatch latch = new CountDownLatch(threads);
+ final CountDownLatch latch = new CountDownLatch(steps);
+
+ // System.out.println("Starting image scale on single thread, waiting for execution to complete...");
+// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null);
+ System.out.printf("Started image scale on %d threads, waiting for execution to complete...\n", threads);
+
+ System.out.print("[");
+ final int dotsPerStep = 78 / steps;
+ for (int j = 0; j < 78 - (steps * dotsPerStep); j++) {
+ System.out.print(".");
+ }
// Resample image in slices
- for (int i = 0; i < threads; i++) {
+ for (int i = 0; i < steps; i++) {
final int inY = i * inStep;
final int outY = i * outStep;
final int inHeight = Math.min(inStep, image.getHeight() - inY);
@@ -200,10 +216,12 @@ public class MappedBufferImage {
try {
BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight);
BufferedImage out = output.getSubimage(0, outY, width, outHeight);
- new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out);
-// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).resample(in, out, ResampleOp.createFilter(ResampleOp.FILTER_LANCZOS));
-// BufferedImage out = new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, null);
-// ImageUtil.drawOnto(output.getSubimage(0, outY, width, outHeight), out);
+ new ResampleOp(width, outHeight, ResampleOp.FILTER_TRIANGLE).filter(in, out);
+// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out);
+
+ for (int j = 0; j < dotsPerStep; j++) {
+ System.out.print(".");
+ }
}
catch (RuntimeException e) {
e.printStackTrace();
@@ -216,19 +234,17 @@ public class MappedBufferImage {
});
}
-// System.out.println("Starting image scale on single thread, waiting for execution to complete...");
-// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null);
- System.out.printf("Started image scale on %d threads, waiting for execution to complete...%n", threads);
-
Boolean done = null;
try {
done = latch.await(5L, TimeUnit.MINUTES);
}
catch (InterruptedException ignore) {
}
+ System.out.println("]");
- System.out.printf("%s scaling image in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start);
+ System.out.printf("%s scaling image in %d ms\n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start);
System.out.println("image = " + output);
+
return output;
}
@@ -358,10 +374,12 @@ public class MappedBufferImage {
private static class ImageComponent extends JComponent implements Scrollable {
private final BufferedImage image;
private Paint texture;
- double zoom = 1;
+ private double zoom = 1;
public ImageComponent(final BufferedImage image) {
- setOpaque(true); // Very important when subclassing JComponent...
+ setOpaque(true); // Very important when sub classing JComponent...
+ setDoubleBuffered(true);
+
this.image = image;
}
@@ -370,6 +388,68 @@ public class MappedBufferImage {
super.addNotify();
texture = createTexture();
+
+ Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
+ zoom = Math.min(1.0, Math.min(bounds.getWidth() / (double) image.getWidth(), bounds.getHeight() / (double) image.getHeight()));
+
+ // TODO: Take scroll pane into account when zooming (center around center point)
+ AbstractAction zoomIn = new AbstractAction() {
+ public void actionPerformed(ActionEvent e) {
+ System.err.println("ZOOM IN");
+ setZoom(zoom * 2);
+ }
+ };
+
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, getToolkit().getMenuShortcutKeyMask()), zoomIn);
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, getToolkit().getMenuShortcutKeyMask()), zoomIn);
+ addAction(KeyStroke.getKeyStroke(Character.valueOf('+'), 0), zoomIn);
+ addAction(KeyStroke.getKeyStroke(Character.valueOf('+'), getToolkit().getMenuShortcutKeyMask()), zoomIn);
+ AbstractAction zoomOut = new AbstractAction() {
+ public void actionPerformed(ActionEvent e) {
+ System.err.println("ZOOM OUT");
+ setZoom(zoom / 2);
+ }
+ };
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, getToolkit().getMenuShortcutKeyMask()), zoomOut);
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, getToolkit().getMenuShortcutKeyMask()), zoomOut);
+ addAction(KeyStroke.getKeyStroke(Character.valueOf('-'), 0), zoomOut);
+ addAction(KeyStroke.getKeyStroke(Character.valueOf('-'), getToolkit().getMenuShortcutKeyMask()), zoomOut);
+ AbstractAction zoomFit = new AbstractAction() {
+ public void actionPerformed(ActionEvent e) {
+ System.err.println("ZOOM FIT");
+// Rectangle bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
+ Rectangle bounds = getVisibleRect();
+ setZoom(Math.min(1.0, Math.min(bounds.getWidth() / (double) image.getWidth(), bounds.getHeight() / (double) image.getHeight())));
+ }
+ };
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, getToolkit().getMenuShortcutKeyMask()), zoomFit);
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_9, getToolkit().getMenuShortcutKeyMask()), zoomFit);
+ addAction(KeyStroke.getKeyStroke(KeyEvent.VK_0, getToolkit().getMenuShortcutKeyMask()), new AbstractAction() {
+ public void actionPerformed(ActionEvent e) {
+ System.err.println("ZOOM ACTUAL");
+ setZoom(1);
+ }
+ });
+ }
+
+ private void setZoom(final double newZoom) {
+ if (newZoom != zoom) {
+ zoom = newZoom;
+ // TODO: Add PCL support for zoom and discard tiles cache based on property change
+ tiles = createTileCache();
+ revalidate();
+ repaint();
+ }
+ }
+
+ private Map createTileCache() {
+ return Collections.synchronizedMap(new SizedLRUMap(16 * 1024 * 1024));
+ }
+
+ private void addAction(final KeyStroke keyStroke, final AbstractAction action) {
+ UUID key = UUID.randomUUID();
+ getInputMap(WHEN_IN_FOCUSED_WINDOW).put(keyStroke, key);
+ getActionMap().put(key, action);
}
private Paint createTexture() {
@@ -392,10 +472,17 @@ public class MappedBufferImage {
@Override
protected void paintComponent(Graphics g) {
+ // TODO: Java 7 kills the performance from our custom painting... :-(
+
// TODO: Figure out why mouse wheel/track pad scroll repaints entire component,
// unlike using the scroll bars of the JScrollPane.
// Consider creating a custom mouse wheel listener as a workaround.
+ // TODO: Cache visible rect content in buffered/volatile image (s) + visible rect (+ zoom) to speed up repaints
+ // - Blit the cahced image (possibly translated) (onto itself?)
+ // - Paint only the necessary parts outside the cached image
+ // - Async rendering into cached image
+
// We want to paint only the visible part of the image
Rectangle visible = getVisibleRect();
Rectangle clip = g.getClipBounds();
@@ -405,9 +492,28 @@ public class MappedBufferImage {
g2.setPaint(texture);
g2.fillRect(rect.x, rect.y, rect.width, rect.height);
+ /*
+ // Center image (might not be the best way to cooperate with the scroll pane)
+ Rectangle imageSize = new Rectangle((int) Math.round(image.getWidth() * zoom), (int) Math.round(image.getHeight() * zoom));
+ if (imageSize.width < getWidth()) {
+ g2.translate((getWidth() - imageSize.width) / 2, 0);
+ }
+ if (imageSize.height < getHeight()) {
+ g2.translate(0, (getHeight() - imageSize.height) / 2);
+ }
+ */
+
+ // Zoom
if (zoom != 1) {
- AffineTransform transform = AffineTransform.getScaleInstance(zoom, zoom);
- g2.setTransform(transform);
+ // NOTE: This helps mostly when scaling up, or scaling down less than 50%
+ g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+
+ rect = new Rectangle(
+ (int) Math.round(rect.x / zoom), (int) Math.round(rect.y / zoom),
+ (int) Math.round(rect.width / zoom), (int) Math.round(rect.height / zoom)
+ );
+
+ rect = rect.intersection(new Rectangle(image.getWidth(), image.getHeight()));
}
long start = System.currentTimeMillis();
@@ -415,39 +521,308 @@ public class MappedBufferImage {
System.err.println("repaint: " + (System.currentTimeMillis() - start) + " ms");
}
- private void repaintImage(Rectangle rect, Graphics2D g2) {
+ static class Tile {
+ private final int size;
+
+ private final int x;
+ private final int y;
+
+ private final Reference data;
+ private final BufferedImage hardRef;
+
+ Tile(int x, int y, BufferedImage data) {
+ this.x = x;
+ this.y = y;
+ this.data = new SoftReference(data);
+
+ hardRef = data;
+
+ size = 16 + data.getWidth() * data.getHeight() * data.getRaster().getNumDataElements() * sizeOf(data.getRaster().getTransferType());
+ }
+
+ private static int sizeOf(final int transferType) {
+ switch (transferType) {
+ case DataBuffer.TYPE_INT:
+ return 4;
+ case DataBuffer.TYPE_SHORT:
+ return 2;
+ case DataBuffer.TYPE_BYTE:
+ return 1;
+ default:
+ throw new IllegalArgumentException("Unsupported transfer type: " + transferType);
+ }
+ }
+
+ public void drawTo(Graphics2D g) {
+ BufferedImage img = data.get();
+
+ if (img != null) {
+ g.drawImage(img, x, y, null);
+ }
+
+// g.setPaint(Color.GREEN);
+// g.drawString(String.format("[%d, %d]", x, y), x + 20, y + 20);
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ public int getWidth() {
+ BufferedImage img = data.get();
+ return img != null ? img.getWidth() : -1;
+ }
+
+ public int getHeight() {
+ BufferedImage img = data.get();
+ return img != null ? img.getHeight() : -1;
+ }
+
+ public Rectangle getRect() {
+ BufferedImage img = data.get();
+ return img != null ? new Rectangle(x, y, img.getWidth(), img.getHeight()) : null;
+ }
+
+ public Point getLocation() {
+ return new Point(x, y);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+
+ Tile tile = (Tile) other;
+
+ return x == tile.x && y == tile.y;
+ }
+
+ @Override
+ public int hashCode() {
+ return 997 * x + y;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Tile[%d, %d, %d, %d]", x, y, getWidth(), getHeight());
+ }
+
+ public int size() {
+ return size;
+ }
+ }
+
+ // TODO: Consider a fixed size (mem) LRUCache instead
+ Map tiles = createTileCache();
+
+ private void repaintImage(final Rectangle rect, final Graphics2D g2) {
+// System.err.println("rect: " + rect);
+// System.err.println("tiles: " + tiles.size());
+ // TODO: Fix rounding errors
+ // FIx repaint bugs
+
try {
// Paint tiles of the image, to preserve memory
- int sliceSize = 200;
+ final int tileSize = 200;
- int slicesW = rect.width / sliceSize;
- int slicesH = rect.height / sliceSize;
+ int tilesW = 1 + rect.width / tileSize;
+ int tilesH = 1 + rect.height / tileSize;
- for (int sliceY = 0; sliceY <= slicesH; sliceY++) {
- for (int sliceX = 0; sliceX <= slicesW; sliceX++) {
- int x = rect.x + sliceX * sliceSize;
- int y = rect.y + sliceY * sliceSize;
+ for (int yTile = 0; yTile <= tilesH; yTile++) {
+ for (int xTile = 0; xTile <= tilesW; xTile++) {
+ // Image (source) coordinates
+ int x = rect.x + xTile * tileSize;
+ int y = rect.y + yTile * tileSize;
- int w = sliceX == slicesW ? Math.min(sliceSize, rect.x + rect.width - x) : sliceSize;
- int h = sliceY == slicesH ? Math.min(sliceSize, rect.y + rect.height - y) : sliceSize;
+ int w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize;
+ int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize;
if (w == 0 || h == 0) {
continue;
}
// System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h);
- BufferedImage img = image.getSubimage(x, y, w, h);
- g2.drawImage(img, x, y, null);
+
+ // - Get tile from cache
+ // - If non-null, paint
+ // - If null, request data for later use, with callback, and return
+ // TODO: Could we use ImageProducer/ImageConsumer/ImageObserver interface??
+
+ // Destination (display) coordinates
+ int dstX = (int) Math.round(x * zoom);
+ int dstY = (int) Math.round(y * zoom);
+ int dstW = (int) Math.round(w * zoom);
+ int dstH = (int) Math.round(h * zoom);
+
+ if (dstW == 0 || dstH == 0) {
+ continue;
+ }
+
+ // Don't create overlapping/duplicate tiles...
+ // - Always start tile grid at 0,0
+ // - Always occupy entire tile, unless edge
+
+ // Source (original) coordinates
+ int tileSrcX = x - x % tileSize;
+ int tileSrcY = y - y % tileSize;
+// final int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX);
+// final int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY);
+
+ // Destination (display) coordinates
+ int tileDstX = (int) Math.round(tileSrcX * zoom);
+ int tileDstY = (int) Math.round(tileSrcY * zoom);
+// final int tileDstW = (int) Math.round(tileSrcW * zoom);
+// final int tileDstH = (int) Math.round(tileSrcH * zoom);
+
+ List points = new ArrayList(4);
+ points.add(new Point(tileDstX, tileDstY));
+ if (tileDstX != dstX) {
+ points.add(new Point(tileDstX + tileSize, tileDstY));
+ }
+ if (tileDstY != dstY) {
+ points.add(new Point(tileDstX, tileDstY + tileSize));
+ }
+ if (tileDstX != dstX && tileDstY != dstY) {
+ points.add(new Point(tileDstX + tileSize, tileDstY + tileSize));
+ }
+
+ for (final Point point : points) {
+ Tile tile = tiles.get(point);
+
+ if (tile != null) {
+ Reference img = tile.data;
+ if (img != null) {
+ tile.drawTo(g2);
+ continue;
+ }
+ else {
+ tiles.remove(point);
+ }
+ }
+
+// System.err.printf("Tile miss: [%d, %d]\n", dstX, dstY);
+
+ // Dispatch to off-thread worker
+ final Map localTiles = tiles;
+ executorService.submit(new Runnable() {
+ public void run() {
+ // TODO: Fix rounding issues... Problem is that sometimes the srcW/srcH is 1 pixel off filling the tile...
+ int tileSrcX = (int) Math.round(point.x / zoom);
+ int tileSrcY = (int) Math.round(point.y / zoom);
+ int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX);
+ int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY);
+ int tileDstW = (int) Math.round(tileSrcW * zoom);
+ int tileDstH = (int) Math.round(tileSrcH * zoom);
+
+ try {
+ // TODO: Consider comparing zoom/local zoom
+ if (localTiles != tiles) {
+ return; // Return early after re-zoom
+ }
+
+ if (localTiles.containsKey(point)) {
+// System.err.println("Skipping tile, already producing...");
+ return;
+ }
+
+ // Test against current view rect, to avoid computing tiles that will be thrown away immediately
+ // TODO: EDT safe?
+ if (!getVisibleRect().intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) {
+ return;
+ }
+
+// System.err.printf("Creating tile: [%d, %d]\n", tileDstX, tileDstY);
+
+ BufferedImage temp = getGraphicsConfiguration().createCompatibleImage(tileDstW, tileDstH);
+ final Tile tile = new Tile(point.x, point.y, temp);
+ localTiles.put(point, tile);
+
+ Graphics2D graphics = temp.createGraphics();
+ try {
+ Object hint = g2.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
+
+ if (hint != null) {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
+ }
+
+ graphics.scale(zoom, zoom);
+ graphics.drawImage(image.getSubimage(tileSrcX, tileSrcY, tileSrcW, tileSrcH), 0, 0, null);
+ }
+ finally {
+ graphics.dispose();
+ }
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ repaint(10, tile.x, tile.y, tile.getWidth(), tile.getHeight());
+ }
+ });
+ }
+ catch (Throwable t) {
+ localTiles.remove(point);
+ System.err.println("Boooo: " + t.getMessage());
+ }
+ }
+ });
+ }
+
}
}
-
-// BufferedImage img = image.getSubimage(rect.x, rect.y, rect.width, rect.height);
-// g2.drawImage(img, rect.x, rect.y, null);
}
catch (NullPointerException e) {
// e.printStackTrace();
- // Happens whenever apple.awt.OSXCachingSufraceManager runs out of memory
+ // Happens whenever apple.awt.OSXCachingSurfaceManager runs out of memory
// TODO: Figure out why repaint(x,y,w,h) doesn't work any more..?
+ System.err.println("Full repaint due to NullPointerException (probably out of memory).");
+ repaint(); // NOTE: Might cause a brief flash while the component is redrawn
+ }
+ }
+
+ private void repaintImage0(final Rectangle rect, final Graphics2D g2) {
+ g2.scale(zoom, zoom);
+
+ try {
+ // Paint tiles of the image, to preserve memory
+ final int tileSize = 200;
+
+ int tilesW = rect.width / tileSize;
+ int tilesH = rect.height / tileSize;
+
+ for (int yTile = 0; yTile <= tilesH; yTile++) {
+ for (int xTile = 0; xTile <= tilesW; xTile++) {
+ // Image (source) coordinates
+ final int x = rect.x + xTile * tileSize;
+ final int y = rect.y + yTile * tileSize;
+
+ final int w = xTile == tilesW ? Math.min(tileSize, rect.x + rect.width - x) : tileSize;
+ final int h = yTile == tilesH ? Math.min(tileSize, rect.y + rect.height - y) : tileSize;
+
+ if (w == 0 || h == 0) {
+ continue;
+ }
+
+// System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h);
+
+ BufferedImage img = image.getSubimage(x, y, w, h);
+ g2.drawImage(img, x, y, null);
+
+ }
+ }
+ }
+ catch (NullPointerException e) {
+// e.printStackTrace();
+ // Happens whenever apple.awt.OSXCachingSurfaceManager runs out of memory
+ // TODO: Figure out why repaint(x,y,w,h) doesn't work any more..?
+ System.err.println("Full repaint due to NullPointerException (probably out of memory).");
repaint(); // NOTE: Might cause a brief flash while the component is redrawn
}
}
@@ -476,12 +851,68 @@ public class MappedBufferImage {
}
public boolean getScrollableTracksViewportWidth() {
- return false;
+ return getWidth() > getPreferredSize().width;
}
public boolean getScrollableTracksViewportHeight() {
+ return getHeight() > getPreferredSize().height;
+ }
+ }
+
+ final static class SizedLRUMap extends LRUHashMap {
+ int currentSize;
+ int maxSize;
+
+ public SizedLRUMap(int pMaxSize) {
+ super(); // Note: super.maxSize doesn't count...
+ maxSize = pMaxSize;
+ }
+
+
+ protected int sizeOf(final Object pValue) {
+ ImageComponent.Tile cached = (ImageComponent.Tile) pValue;
+
+ if (cached == null) {
+ return 0;
+ }
+
+ return cached.size();
+ }
+
+ @Override
+ public V put(K pKey, V pValue) {
+ currentSize += sizeOf(pValue);
+
+ V old = super.put(pKey, pValue);
+ if (old != null) {
+ currentSize -= sizeOf(old);
+ }
+ return old;
+ }
+
+ @Override
+ public V remove(Object pKey) {
+ V old = super.remove(pKey);
+ if (old != null) {
+ currentSize -= sizeOf(old);
+ }
+ return old;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry pEldest) {
+ if (maxSize <= currentSize) { // NOTE: maxSize here is mem size
+ removeLRU();
+ }
return false;
}
+
+ @Override
+ public void removeLRU() {
+ while (maxSize <= currentSize) { // NOTE: maxSize here is mem size
+ super.removeLRU();
+ }
+ }
}
private static class PaintDotsTask implements Runnable {
diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java
index c35ece68..e87673b7 100755
--- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java
+++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java
@@ -28,6 +28,13 @@
package com.twelvemonkeys.util;
+import com.twelvemonkeys.io.FileUtil;
+
+import java.io.*;
+import java.util.*;
+
+import static com.twelvemonkeys.lang.Validate.notNull;
+
/**
* PersistentMap
*
@@ -35,27 +42,293 @@ package com.twelvemonkeys.util;
* @author last modified by $Author: haraldk$
* @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$
*/
-public class PersistentMap {
- // TODO: Implement Map
- // TODO: Delta synchronization (db?)
+public class PersistentMap extends AbstractMap{
+ public static final FileFilter DIRECTORIES = new FileFilter() {
+ public boolean accept(File file) {
+ return file.isDirectory();
+ }
+
+ @Override
+ public String toString() {
+ return "[All folders]";
+ }
+ };
+ private static final String INDEX = ".index";
+
+ private final File root;
+ private final Map index = new LinkedHashMap();
+
+ private boolean mutable = true;
+
+
+ // Idea 2.0:
+ // - Create directory per hashCode
+ // - Create file per object in that directory
+ // - Name file after serialized form of key? Base64?
+ // - Special case for String/Integer/Long etc?
+ // - Or create index file in directory with serialized objects + name (uuid) of file
+
+ // TODO: Consider single index file? Or a few? In root directory instead of each directory
+ // Consider a RAF/FileChannel approach instead of streams - how do we discard portions of a RAF?
+ // - Need to keep track of used/unused parts of file, scan for gaps etc...?
+ // - Need to periodically truncate and re-build the index (always as startup, then at every N puts/removes?)
+
+ /*public */PersistentMap(String id) {
+ this(new File(FileUtil.getTempDirFile(), id));
+ }
+
+ public PersistentMap(File root) {
+ this.root = notNull(root);
+
+ init();
+ }
+
+ private void init() {
+ if (!root.exists() && !root.mkdirs()) {
+ throw new IllegalStateException(String.format("'%s' does not exist/could not be created", root.getAbsolutePath()));
+ }
+ else if (!root.isDirectory()) {
+ throw new IllegalStateException(String.format("'%s' exists but is not a directory", root.getAbsolutePath()));
+ }
+
+ if (!root.canRead()) {
+ throw new IllegalStateException(String.format("'%s' is not readable", root.getAbsolutePath()));
+ }
+
+ if (!root.canWrite()) {
+ mutable = false;
+ }
+
+ FileUtil.visitFiles(root, DIRECTORIES, new Visitor() {
+ public void visit(File dir) {
+ // - Read .index file
+ // - Add entries to index
+ ObjectInputStream input = null;
+ try {
+ input = new ObjectInputStream(new FileInputStream(new File(dir, INDEX)));
+ while (true) {
+ @SuppressWarnings({"unchecked"})
+ K key = (K) input.readObject();
+ String fileName = (String) input.readObject();
+ index.put(key, UUID.fromString(fileName));
+ }
+ }
+ catch (EOFException eof) {
+ // break here
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ FileUtil.close(input);
+ }
+ }
+ });
+ }
+
+ @Override
+ public Set> entrySet() {
+ return new AbstractSet>() {
+ @Override
+ public Iterator> iterator() {
+ return new Iterator>() {
+ Iterator> indexIter = index.entrySet().iterator();
+
+ public boolean hasNext() {
+ return indexIter.hasNext();
+ }
+
+ public Entry next() {
+ return new Entry() {
+ final Entry entry = indexIter.next();
+
+ public K getKey() {
+ return entry.getKey();
+ }
+
+ public V getValue() {
+ K key = entry.getKey();
+ int hash = key != null ? key.hashCode() : 0;
+ return readVal(hash, entry.getValue());
+ }
+
+ public V setValue(V value) {
+ K key = entry.getKey();
+ int hash = key != null ? key.hashCode() : 0;
+ return writeVal(key, hash, entry.getValue(), value, getValue());
+ }
+ };
+ }
+
+ public void remove() {
+ indexIter.remove();
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return index.size();
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return index.size();
+ }
+
+ @Override
+ public V put(K key, V value) {
+ V oldVal = null;
+
+ UUID uuid = index.get(key);
+ int hash = key != null ? key.hashCode() : 0;
+
+ if (uuid != null) {
+ oldVal = readVal(hash, uuid);
+ }
+
+ return writeVal(key, hash, uuid, value, oldVal);
+ }
+
+ private V writeVal(K key, int hash, UUID uuid, V value, V oldVal) {
+ if (!mutable) {
+ throw new UnsupportedOperationException();
+ }
+
+ File bucket = new File(root, hashToFileName(hash));
+ if (!bucket.exists() && !bucket.mkdirs()) {
+ throw new IllegalStateException(String.format("Could not create bucket '%s'", bucket));
+ }
+
+ if (uuid == null) {
+ // No uuid means new entry
+ uuid = UUID.randomUUID();
+
+ File idx = new File(bucket, INDEX);
+
+ ObjectOutputStream output = null;
+ try {
+ output = new ObjectOutputStream(new FileOutputStream(idx, true));
+ output.writeObject(key);
+ output.writeObject(uuid.toString());
+
+ index.put(key, uuid);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ FileUtil.close(output);
+ }
+ }
+
+ File entry = new File(bucket, uuid.toString());
+ if (value != null) {
+ ObjectOutputStream output = null;
+ try {
+ output = new ObjectOutputStream(new FileOutputStream(entry));
+ output.writeObject(value);
+
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ FileUtil.close(output);
+ }
+ }
+ else if (entry.exists()) {
+ if (!entry.delete()) {
+ throw new IllegalStateException(String.format("'%s' could not be deleted", entry));
+ }
+ }
+
+ return oldVal;
+ }
+
+ private String hashToFileName(int hash) {
+ return Integer.toString(hash, 16);
+ }
+
+ @Override
+ public V get(Object key) {
+ UUID uuid = index.get(key);
+
+ if (uuid != null) {
+ int hash = key != null ? key.hashCode() : 0;
+ return readVal(hash, uuid);
+ }
+
+ return null;
+ }
+
+ private V readVal(final int hash, final UUID uuid) {
+ File bucket = new File(root, hashToFileName(hash));
+ File entry = new File(bucket, uuid.toString());
+
+ if (entry.exists()) {
+ ObjectInputStream input = null;
+ try {
+ input = new ObjectInputStream(new FileInputStream(entry));
+ //noinspection unchecked
+ return (V) input.readObject();
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ FileUtil.close(input);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public V remove(Object key) {
+ // TODO!!!
+ return super.remove(key);
+ }
+
+ // TODO: Should override size, put, get, remove, containsKey and containsValue
+
+
+
}
+
+
/*
+Memory mapped file?
+Delta sync?
+
Persistent format
Header
File ID 4-8 bytes
- Size
+ Size (entries)
- Entry pointer array block
- Size
- Next entry pointer block address
- Entry 1 address
+ PersistentEntry pointer array block (PersistentEntry 0)
+ Size (bytes)
+ Next entry pointer block address (0 if last)
+ PersistentEntry 1 address/offset + key
...
- Entry n address
+ PersistentEntry n address/offset + key
+
+ PersistentEntry 1
+ Size (bytes)?
+ Serialized value or pointer array block
+ ...
+ PersistentEntry n
+ Size (bytes)?
+ Serialized value or pointer array block
- Entry 1
- ...
- Entry n
-
*/
\ No newline at end of file