cache images to improve rendering performance

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java
index 379802e..c765b76 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java
@@ -70,6 +70,9 @@
 		}
 	};
 
+	public void resetOrigin() {
+	}
+
 	public void moveOrigin(final int offsetX, final int offsetY) {
 	}
 
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java
index 45668f0..bb6331c 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java
@@ -67,6 +67,11 @@
 	}
 
 	@Override
+	public void resetOrigin() {
+		tracer.trace("Graphics.resetOrigin()");
+	}
+
+	@Override
 	public void moveOrigin(final int offsetX, final int offsetY) {
 		tracer.trace("Graphics.moveOrigin({0}, {1})", offsetX, offsetY);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java
index 4dd6e5b..77025de 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java
@@ -28,6 +28,8 @@
 
 	public void dispose();
 
+	public void resetOrigin();
+
 	public void moveOrigin(int offsetX, int offsetY);
 
 	public int asAbsoluteX(int relativeX);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java
index 20f906d..cf5c0aa 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2008 John Krasnay and others.
+ * Copyright (c) 2004, 2016 John Krasnay and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     John Krasnay - initial API and implementation
+ *     Florian Thienel - hashCode, equals, toString
  *******************************************************************************/
 package org.eclipse.vex.core.internal.core;
 
@@ -19,8 +20,6 @@
 	private final int y;
 
 	/**
-	 * Class constructor.
-	 *
 	 * @param x
 	 *            X-coordinate.
 	 * @param y
@@ -31,30 +30,52 @@
 		this.y = y;
 	}
 
-	@Override
-	public String toString() {
-		final StringBuffer sb = new StringBuffer(80);
-		sb.append(Point.class.getName());
-		sb.append("[x=");
-		sb.append(getX());
-		sb.append(",y=");
-		sb.append(getY());
-		sb.append("]");
-		return sb.toString();
-	}
-
 	/**
-	 * Returns the x-coordinate.
+	 * @return the x-coordinate.
 	 */
 	public int getX() {
 		return x;
 	}
 
 	/**
-	 * Returns the y-coordinate.
+	 * @return the y-coordinate.
 	 */
 	public int getY() {
 		return y;
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + x;
+		result = prime * result + y;
+		return result;
+	}
+
+	@Override
+	public boolean equals(final Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final Point other = (Point) obj;
+		if (x != other.x) {
+			return false;
+		}
+		if (y != other.y) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Point [x=" + x + ", y=" + y + "]";
+	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java
index f92249a..abfc4f2 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java
@@ -10,13 +10,19 @@
  *******************************************************************************/
 package org.eclipse.vex.core.internal.widget.swt;
 
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.eclipse.swt.events.DisposeEvent;
 import org.eclipse.swt.events.DisposeListener;
 import org.eclipse.swt.events.PaintEvent;
 import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Device;
 import org.eclipse.swt.graphics.GC;
 import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.widgets.Scrollable;
+import org.eclipse.vex.core.internal.core.Color;
 import org.eclipse.vex.core.internal.core.Graphics;
 import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.widget.IRenderer;
@@ -35,8 +41,11 @@
 
 	private final Scrollable control;
 
-	private Image bufferImage;
 	private final Object bufferMonitor = new Object();
+	private final RenderBuffer[] buffer = new RenderBuffer[2];
+	private int visibleIndex = 0;
+
+	private final Map<URL, SwtImage> imageCache = new HashMap<URL, SwtImage>();
 
 	public DoubleBufferedRenderer(final Scrollable control) {
 		this.control = control;
@@ -56,63 +65,79 @@
 
 	private void widgetDisposed(final DisposeEvent e) {
 		synchronized (bufferMonitor) {
-			if (bufferImage != null) {
-				bufferImage.dispose();
+			for (int i = 0; i < buffer.length; i += 1) {
+				if (buffer[i] != null) {
+					buffer[i].dispose();
+				}
+				buffer[i] = null;
 			}
-			bufferImage = null;
 		}
 	}
 
 	private void paintControl(final PaintEvent event) {
-		event.gc.drawImage(getBufferImage(), 0, 0);
+		event.gc.drawImage(getVisibleImage(), 0, 0);
 	}
 
-	private Image getBufferImage() {
+	private Image getVisibleImage() {
 		synchronized (bufferMonitor) {
-			if (bufferImage == null) {
-				bufferImage = createImage(Rectangle.NULL);
+			if (buffer[visibleIndex] == null) {
+				buffer[visibleIndex] = createRenderBuffer(Rectangle.NULL);
 			}
-			return bufferImage;
+			return buffer[visibleIndex].image;
 		}
 	}
 
+	private RenderBuffer getRenderBuffer(final Rectangle viewPort) {
+		synchronized (bufferMonitor) {
+			final int index = (visibleIndex + 1) % 2;
+			if (buffer[index] == null) {
+				buffer[index] = createRenderBuffer(viewPort);
+			} else if (!viewPortFitsIntoImage(viewPort, buffer[index].image)) {
+				buffer[index].dispose();
+				buffer[index] = createRenderBuffer(viewPort);
+			}
+			return buffer[index];
+		}
+	}
+
+	private static boolean viewPortFitsIntoImage(final Rectangle viewPort, final Image image) {
+		return image.getBounds().contains(viewPort.getWidth() - 1, viewPort.getHeight() - 1);
+	}
+
 	@Override
 	public void render(final Rectangle viewPort, final IRenderStep... steps) {
-		final Image image = createImage(viewPort);
-		final GC gc = new GC(image);
-		final Graphics graphics = new SwtGraphics(gc);
+		final RenderBuffer buffer = getRenderBuffer(viewPort);
 
-		moveOriginToViewPort(viewPort, graphics);
+		buffer.graphics.resetOrigin();
+		clearViewPort(viewPort, buffer.graphics);
+		moveOriginToViewPort(viewPort, buffer.graphics);
 
-		try {
-			for (final IRenderStep step : steps) {
-				try {
-					step.render(graphics);
-				} catch (final Throwable t) {
-					t.printStackTrace(); //TODO proper logging
-				}
+		for (final IRenderStep step : steps) {
+			try {
+				step.render(buffer.graphics);
+			} catch (final Throwable t) {
+				t.printStackTrace(); //TODO proper logging
 			}
-		} finally {
-			graphics.dispose();
-			gc.dispose();
 		}
 
-		makeRenderedImageVisible(image);
+		makeRenderedImageVisible(buffer.image);
 	}
 
-	private Image createImage(final Rectangle viewPort) {
-		return new Image(control.getDisplay(), Math.max(1, viewPort.getWidth()), Math.max(1, viewPort.getHeight()));
+	private RenderBuffer createRenderBuffer(final Rectangle viewPort) {
+		return new RenderBuffer(control.getDisplay(), viewPort.getWidth(), viewPort.getHeight(), imageCache);
 	}
 
 	private void moveOriginToViewPort(final Rectangle viewPort, final Graphics graphics) {
 		graphics.moveOrigin(0, -viewPort.getY());
 	}
 
+	private void clearViewPort(final Rectangle viewPort, final Graphics graphics) {
+		graphics.setColor(graphics.getColor(Color.WHITE));
+		graphics.fillRect(0, 0, viewPort.getWidth(), viewPort.getHeight());
+	}
+
 	private void makeRenderedImageVisible(final Image newImage) {
-		final Image oldImage = swapBufferImage(newImage);
-		if (oldImage != null) {
-			oldImage.dispose();
-		}
+		swapBufferImage(newImage);
 		control.getDisplay().syncExec(new Runnable() {
 			@Override
 			public void run() {
@@ -121,12 +146,27 @@
 		});
 	}
 
-	private Image swapBufferImage(final Image newImage) {
+	private void swapBufferImage(final Image newImage) {
 		synchronized (bufferMonitor) {
-			final Image oldImage = bufferImage;
-			bufferImage = newImage;
-			return oldImage;
+			visibleIndex = (visibleIndex + 1) % 2;
 		}
 	}
 
+	private static class RenderBuffer {
+		public final Image image;
+		public final GC gc;
+		public final SwtGraphics graphics;
+
+		public RenderBuffer(final Device device, final int width, final int height, final Map<URL, SwtImage> imageCache) {
+			image = new Image(device, Math.max(1, width), Math.max(1, height));
+			gc = new GC(image);
+			graphics = new SwtGraphics(gc, imageCache);
+		}
+
+		public void dispose() {
+			graphics.dispose();
+			gc.dispose();
+			image.dispose();
+		}
+	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java
index 5d504bc..d219441 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java
@@ -17,6 +17,7 @@
 import java.net.URL;
 import java.text.MessageFormat;
 import java.util.HashMap;
+import java.util.Map;
 
 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IStatus;
@@ -45,11 +46,14 @@
 public class SwtGraphics implements Graphics {
 
 	private final GC gc;
+	private final Map<URL, SwtImage> imageCache;
+
 	private int offsetX;
 	private int offsetY;
 
 	private final HashMap<FontSpec, FontResource> fonts = new HashMap<FontSpec, FontResource>();
 	private final HashMap<Color, ColorResource> colors = new HashMap<Color, ColorResource>();
+	private final HashMap<URL, org.eclipse.swt.graphics.Image> images = new HashMap<URL, org.eclipse.swt.graphics.Image>();
 
 	private SwtFont currentFont;
 	private SwtFontMetrics currentFontMetrics;
@@ -59,8 +63,19 @@
 	 * @param gc
 	 *            SWT GC to which we are drawing.
 	 */
+	@Deprecated
 	public SwtGraphics(final GC gc) {
+		this(gc, new HashMap<URL, SwtImage>());
+	}
+
+	/**
+	 * @param gc
+	 *            SWT GC to which we are drawing.
+	 */
+	public SwtGraphics(final GC gc, final Map<URL, SwtImage> imageCache) {
 		this.gc = gc;
+		this.imageCache = imageCache;
+
 		currentFont = new SwtFont(gc.getFont());
 	}
 
@@ -74,12 +89,22 @@
 			color.dispose();
 		}
 		colors.clear();
+		for (final org.eclipse.swt.graphics.Image image : images.values()) {
+			image.dispose();
+		}
+		images.clear();
 
 		// TODO should not dispose something that comes from outside!
 		gc.dispose();
 	}
 
 	@Override
+	public void resetOrigin() {
+		offsetX = 0;
+		offsetY = 0;
+	}
+
+	@Override
 	public void moveOrigin(final int offsetX, final int offsetY) {
 		this.offsetX += offsetX;
 		this.offsetY += offsetY;
@@ -155,12 +180,18 @@
 	@Override
 	public void drawImage(final Image image, final int x, final int y, final int width, final int height) {
 		Assert.isTrue(image instanceof SwtImage);
-		final org.eclipse.swt.graphics.Image swtImage = new org.eclipse.swt.graphics.Image(gc.getDevice(), ((SwtImage) image).imageData);
-		try {
-			gc.drawImage(swtImage, 0, 0, image.getWidth(), image.getHeight(), x + offsetX, y + offsetY, width, height);
-		} finally {
-			swtImage.dispose();
+		final org.eclipse.swt.graphics.Image swtImage = toSWT((SwtImage) image);
+		gc.drawImage(swtImage, 0, 0, image.getWidth(), image.getHeight(), x + offsetX, y + offsetY, width, height);
+	}
+
+	private org.eclipse.swt.graphics.Image toSWT(final SwtImage image) {
+		final org.eclipse.swt.graphics.Image cachedImage = images.get(image.url);
+		if (cachedImage != null) {
+			return cachedImage;
 		}
+		final org.eclipse.swt.graphics.Image newImage = new org.eclipse.swt.graphics.Image(gc.getDevice(), image.imageData);
+		images.put(image.url, newImage);
+		return newImage;
 	}
 
 	/**
@@ -232,11 +263,19 @@
 
 	@Override
 	public Image getImage(final URL url) {
+		final SwtImage cachedImage = imageCache.get(url);
+		if (cachedImage != null) {
+			return cachedImage;
+		}
+
 		final ImageData[] imageData = loadImageData(url);
 		if (imageData != null && imageData.length > 0) {
-			return new SwtImage(imageData[0]);
+			final SwtImage loadedImage = new SwtImage(url, imageData[0]);
+			imageCache.put(url, loadedImage);
+			return loadedImage;
 		}
-		return new SwtImage(Display.getDefault().getSystemImage(SWT.ICON_ERROR).getImageData());
+
+		return new SwtImage(url, Display.getDefault().getSystemImage(SWT.ICON_ERROR).getImageData());
 	}
 
 	private static ImageData[] loadImageData(final URL url) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java
index 6e9edc4..2987b04 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java
@@ -1,36 +1,40 @@
-/*******************************************************************************

- * Copyright (c) 2010 Florian Thienel and others.

- * All rights reserved. This program and the accompanying materials

- * are made available under the terms of the Eclipse Public License v1.0

- * which accompanies this distribution, and is available at

- * http://www.eclipse.org/legal/epl-v10.html

- *

- * Contributors:

- * 		Florian Thienel - initial API and implementation

- *******************************************************************************/

-package org.eclipse.vex.core.internal.widget.swt;

-

-import org.eclipse.swt.graphics.ImageData;

-import org.eclipse.vex.core.internal.core.Image;

-

-/**

- * @author Florian Thienel

- */

-public class SwtImage implements Image {

-

-	public final ImageData imageData;

-

-	public SwtImage(final ImageData imageData) {

-		this.imageData = imageData;

-	}

-

-	@Override

-	public int getHeight() {

-		return imageData.height;

-	}

-

-	@Override

-	public int getWidth() {

-		return imageData.width;

-	}

-}

+/*******************************************************************************
+ * Copyright (c) 2010, 2016 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.widget.swt;
+
+import java.net.URL;
+
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.vex.core.internal.core.Image;
+
+/**
+ * @author Florian Thienel
+ */
+public class SwtImage implements Image {
+
+	public final URL url;
+	public final ImageData imageData;
+
+	public SwtImage(final URL url, final ImageData imageData) {
+		this.url = url;
+		this.imageData = imageData;
+	}
+
+	@Override
+	public int getHeight() {
+		return imageData.height;
+	}
+
+	@Override
+	public int getWidth() {
+		return imageData.width;
+	}
+}