/* * 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; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.awt.image.Raster; import java.awt.image.TileObserver; import java.awt.image.WritableRaster; import com.t3.client.ui.zone.PlayerView; import com.t3.client.ui.zone.ZoneRenderer; /** * This is a read-only implementation of BufferedImage intended to * be used with ImageWriters to create very large graphics * files. Since this is read-only, it gets its pixel data * by rasterizing a Zone piece-meal as the data is requested * by getData(rectangle) calls. * * A very simple 4 MB cache is implemented to reduce zoneRender calls. */ public class ZoneImageGenerator extends BufferedImage { // final JComponent largeComponent; final ZoneRenderer renderer; final PlayerView view; final Rectangle origBounds; final int maxCacheSize = 1024*1024; // probably 4MB for most systems Raster cachedRaster; Rectangle cachedRect; Rectangle prevCacheRect; int numMisses = 0; // total number of cache misses, ever int recentHits = 0; // hits since last cache miss public ZoneImageGenerator(ZoneRenderer renderer, PlayerView view) { // The BufferedImage raster made by super() is just a dummy. // Making it something reasonable to avoid edge-case errors. super(32, 32, Transparency.OPAQUE); this.renderer = renderer; this.view = view; origBounds = new Rectangle(renderer.getBounds()); } private boolean cacheMiss(Rectangle rect) { boolean miss; miss = (cachedRect == null) || !cachedRect.contains(rect); return miss; } @Override public Raster getData(Rectangle rect) { if (cacheMiss(rect)) { // Figure out what slice of the Zone to cache int sizeX = rect.width; int sizeY = rect.height; // Let's first try making the cache as wide as the whole zone sizeX = Math.max(sizeX, renderer.getBounds().width); sizeY = Math.max(sizeY, (maxCacheSize / sizeX)); if ((sizeX * sizeY) > maxCacheSize) { // That didn't work, so let's try just making it as wide as requested // and as tall as possible sizeX = rect.width; sizeY = (maxCacheSize / sizeX); } // And let's make sure not to overdo things: no need for the cache to // be larger than the zone itself! sizeX = Math.min(sizeX, origBounds.width); sizeY = Math.min(sizeY, origBounds.height); prevCacheRect = cachedRect; cachedRect = new Rectangle(rect.x, rect.y, sizeX, sizeY); if (cacheMiss(rect)) { assert false: "Ooops! Cache doesn't contain requested data: wanted " + rect + " but have: " + cachedRect; } fillCache(); numMisses++; recentHits = 0; } recentHits++; return cachedRaster.createChild( rect.x, rect.y, // source upper-left X, Y rect.width, rect.height, // size rect.x, rect.y, // child upper-left X, Y (this is 'translated', not actual pixel coords. // The returned raster is only as big as width * height null); } private void fillCache() { Rectangle rect = cachedRect; if ((recentHits == 0) && (numMisses > 0)) { assert false: "Cache is being thrashed: " + prevCacheRect + cachedRect; } // preserve settings Scale origScale = new Scale(renderer.getZoneScale()); Rectangle origBounds = new Rectangle(renderer.getBounds()); // set new temp vars Scale s = new Scale(origScale); s.setOffset( origScale.getOffsetX() - rect.x, origScale.getOffsetY() - rect.y); renderer.setZoneScale(s); renderer.setBounds(rect); // make a tiny buffered image for this (hopefully) small rectangle request BufferedImage image = new BufferedImage(rect.width, rect.height, super.getType()); Graphics2D g = image.createGraphics(); g.setClip(0, 0, rect.width, rect.height); renderer.renderZone(g, view); // dispose is probably not needed. According to javadocs g's are disposed automatically when used in paint() g.dispose(); // makes a copy of the raster... Raster raster = image.getData(); // ...so we can nudge it back to the original coordinate system cachedRaster = raster.createTranslatedChild(rect.x, rect.y); image = null; raster = null; renderer.setBounds(origBounds); renderer.setZoneScale(origScale); } /////////////////////////////////////////////////////////////////////// // All of the methods after this are various forms of no-op designed // to ensure the application fails in a predictable way // if it tries to write to this object. // As noted above, this is a read-only object! /////////////////////////////////////////////////////////////////////// /** * This object cannot be written to. Method does nothing. */ @Override public WritableRaster getRaster() { return null; } /** * This object cannot be written to. Method does nothing. */ @Override public synchronized void setRGB(int x, int y, int rgb) { // Do nothing, since this object is read-only. // I would like to throw an exception, but I can't since BufferedImage doesn't } /** * This object cannot be written to. Method does nothing. */ @Override public void setRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { // Do nothing, since this object is read-only. // I would like to throw an exception, but I can't since BufferedImage doesn't } /** * To the outside world, we represent that this image is as large * as the Component. */ @Override public int getWidth() { return origBounds.width; } /** * To the outside world, we represent that this image is as large * as the Component. */ @Override public int getHeight() { return origBounds.height; } /** * To the outside world, we represent that this image is as large * as the Component. */ @Override public int getWidth(ImageObserver observer) { return this.getWidth(); } /** * To the outside world, we represent that this image is as large * as the Component. */ @Override public int getHeight(ImageObserver observer) { return this.getHeight(); } /** * This object cannot be written to. Returns null. */ @Override public Graphics getGraphics() { return null; } /** * This object cannot be written to. Returns null. */ @Override public Graphics2D createGraphics() { return null; } @Override public Raster getData() { assert false: "Can not get a raster for the whole CachedComponentImage!"; return null; } /** * This should not be needed... hopefully all ImageWriters use getData instead. */ @Override public WritableRaster copyData(WritableRaster outRaster) { if (outRaster == null) { assert false: "Someone tried to get a copy of the whole Raster in CachedComponentImage"; } else { assert false: "Class CachedComponentImage.copyData() called. This should probably be implemented. "; } return null; } /** * This object cannot be written to. Method does nothing. */ @Override public void setData(Raster r) { // Do nothing, since this object is read-only. // I would like to throw an exception, but I can't since BufferedImage doesn't } // These methods are // // @Override public void removeTileObserver(TileObserver to) { super.removeTileObserver(to); // Ha! In BufferedImage image this is a no-op! } /** * This object cannot be written to. Returns false. */ @Override public boolean isTileWritable(int tileX, int tileY) { return false; } /** * This object cannot be written to. Returns null. */ @Override public Point[] getWritableTileIndices() { return null; } /** * This object cannot be written to. Returns false. */ @Override public boolean hasTileWriters() { return false; } /** * This object cannot be written to. Returns null. */ @Override public WritableRaster getWritableTile(int tileX, int tileY) { return null; } /** * This object cannot be written to. Method does nothing. */ @Override public void releaseWritableTile(int tileX, int tileY) { // Do nothing, since this object is read-only. // I would like to throw an exception, but I can't since BufferedImage doesn't } }