/* Copyright 2006 by Sean Luke and George Mason University Licensed under the Academic Free License version 3.0 See the file "LICENSE" for more information */ package sim.portrayal.grid; import sim.portrayal.*; import sim.field.grid.*; import java.awt.*; import java.awt.image.*; import java.awt.geom.*; import sim.util.gui.ColorMap; import sim.util.*; // we don't benefit from being a subclass of HexaValueGridPortrayal2D, but // it makes us easily swappable in (see HexaBugs for example). And also // consistent with the subclass relationship between ValueGridPortrayal2D // and FastValueGridPortrayal2D. public class FastHexaValueGridPortrayal2D extends HexaValueGridPortrayal2D { /** If immutableField is true, we presume that the grid doesn't change. This allows us to just re-splat the buffer. */ public FastHexaValueGridPortrayal2D(String valueName, boolean immutableField) { super(valueName); setImmutableField(immutableField); } public FastHexaValueGridPortrayal2D(String valueName) { this(valueName,false); } /** If immutableField is true, we presume that the grid doesn't change. This allows us to just re-splat the buffer. */ public FastHexaValueGridPortrayal2D(boolean immutableField) { super(); setImmutableField(immutableField); } public FastHexaValueGridPortrayal2D() { this(false); } /* public void reset() { synchronized(this) { buffer = null; } } */ // Determines if we should buffer boolean shouldBuffer(Graphics2D graphics) { // We can either draw lots of rects, or we can pixels to a small bitmap, then // stretch the bitmap into rects using drawImage. Which technique is faster depends // on the OS unfortunately. Solaris prefers the bitmap. Linux prefers the rects // very much. Windows prefers the rects, // except for small draws where bitmaps have a slight edge (which we'll not consider). // MacOS X prefers bitmaps, but will not stretch and draw to an image buffer // without doing fancy-pants interpolation which looks horrible, so we have to check for that. // For now we'll do: // The user can override us if he likes in the options pane. Otherwise... // in Windows, only use the buffer if it's an immutable grid // in MacOS X, use the buffer for all non-image writes ONLY // in X Windows don't use the buffer ever // ...this puts Solaris at a disadvantage but given that Linux is more common... int buffering = getBuffering(); if (buffering==USE_BUFFER) return true; else if (buffering==DONT_USE_BUFFER) return false; else if (sim.display.Display2D.isMacOSX) return (graphics.getDeviceConfiguration(). getDevice().getType() != GraphicsDevice.TYPE_IMAGE_BUFFER); else if (sim.display.Display2D.isWindows) return (immutableField); else // it's Linux or Solaris { return (graphics.getDeviceConfiguration(). getDevice().getType() != GraphicsDevice.TYPE_IMAGE_BUFFER); } } BufferedImage buffer; WritableRaster raster; DataBufferInt dbuffer; // our object to pass to the portrayal final MutableDouble valueToPass = new MutableDouble(0); protected void hitOrDraw(Graphics2D graphics, DrawInfo2D info, Bag putInHere) { final Grid2D field = (Grid2D)this.field; if (field==null) return; final boolean isDoubleGrid2D = (field instanceof DoubleGrid2D); final int maxX = field.getWidth(); final int maxY = field.getHeight(); if (maxX == 0 || maxY == 0) return; final double divideByX = ((maxX%2==0)?(3.0*maxX/2.0+0.5):(3.0*maxX/2.0+2.0)); final double scaleWidth = 1.5 * info.draw.width / ((maxX%2==0)?(3.0*maxX/2.0+0.5):(3.0*maxX/2.0+2.0)); final double translateWidth = info.draw.width / divideByX - scaleWidth/2.0; final double[][] doubleField = (isDoubleGrid2D ? ((DoubleGrid2D) field).field : null); final int[][] intField = (isDoubleGrid2D ? null : ((IntGrid2D) field).field); // final double xScale = info.draw.width / maxX; final double yScale = info.draw.height / (2*maxY+1); double startxd = ((info.clip.x - translateWidth - info.draw.x) / scaleWidth); double startyd = ((info.clip.y - info.draw.y) / (2*yScale)) - 1; double endxd = ((info.clip.x - translateWidth - info.draw.x + info.clip.width) / scaleWidth); double endyd = ((info.clip.y - info.draw.y + info.clip.height) / (2*yScale)); int startx = (int)(startxd); int starty = (int)(startyd); int endx = ((int)(endxd)) + /*2*/ 1; // with rounding, width may be as much as 1 off int endy = ((int)(endyd)) + /*2*/ 1; // with rounding, height may be as much as 1 off // next we determine if this is a DoubleGrid2D or an IntGrid2D // final Rectangle clip = (graphics==null ? null : graphics.getClipBounds()); if (graphics!=null && shouldBuffer(graphics)) { // create new buffer if needed boolean newBuffer = false; //synchronized(this) // { if (buffer==null || buffer.getWidth() != maxX || buffer.getHeight() != (2*maxY+1)) { // interestingly, this is not quite as fast as just making a BufferedImage directly! // at present, transparent images can't take advantage of new Sun efficiency improvements. // Perhaps we should have a new option for opaque images... //buffer = graphics.getDeviceConfiguration().createCompatibleImage(maxX,(2*maxY+1),Transparency.TRANSLUCENT); // oops, it looks like createCompatibleImage has big-time HILARIOUS bugs on OS X Java 1.3.1! // So for the time being we're sticking with the (very slightly faster) // new BufferedImage(...) if (buffer != null) buffer.flush(); // in case Java forgets to clear memory -- bug in OS X buffer = new BufferedImage(maxX,(2*maxY+1),BufferedImage.TYPE_INT_ARGB); // transparency allowed // I had thought that TYPE_INT_ARGB_PRE would be faster because // it's natively supported by MacOS X CoreGraphics so no optimization needs to be done // in 1.4.1 -- but in fact it is SLOWER on 1.3.1 by 2/3. So for the time being we're // going to stay with the orgiginal. // see http://developer.apple.com/documentation/Java/Reference/Java14SysProperties/System_Properties/chapter_2_section_6.html newBuffer = true; raster = buffer.getRaster(); dbuffer = (DataBufferInt)(raster.getDataBuffer()); } // } //WritableRaster _raster = raster; DataBufferInt _dbuffer = dbuffer; if (newBuffer || !immutableField || isDirtyField()) // we have to load the buffer { if (endx > maxX) endx = maxX; if (endy > maxY) endy = maxY; if( startx < 0 ) startx = 0; if( starty < 0 ) starty = 0; if (immutableField) { // must load ENTIRE buffer startx = 0; starty = 0; endx = maxX; endy = maxY; } final int ex = endx; final int ey = endy; final int sx = startx; final int sy = starty; final ColorMap map = this.map; // Some history here. We initially started by using setRGB in BufferedImage. // But based on some hints, we dug down. First we grabbed the Raster and // used setDataElements, which gave us a big speed boost. // Now we're digging even further down and grabbing the data buffer, which // we know is a DataBufferInt, using a scanlineStride as shown. The DataBufferInt // docs show how to compute which value in the (one-dimensional) data buffer to // poke in order to set the equivalent pixel value. This gives us a slight // improvement. We've since deleted the setRGB code, but we've kept the more // readable setDataElements code in comments because the scanlineStride code // is so illegible. :-) // // FastValueGridPortrayal2D has a significantly faster method still: directly setting elements // in the data array. But while this works great for rectangular regions, it's // quite complex to do with hexagonal stuff so instead we're just setting the // various pixels via setElem as we go. // // Apparently for opaque BufferedImages in Sun implementations (Windows, etc.), // there's a new "Managed Buffer" notion -- if you don't extract the buffer, they // do what they can to make things as fast for you as possible -- but it doesn't // help in the case of transparent images. If we add a transparency constructor, // we should undo the dbuffer code and go back to the raster code and see if that's // faster in Windows in 1.4.2 and on. See // http://weblogs.java.net/blog/chet/archive/2003/08/bufferedimage_a_1.html int load; int scanlineStride = ((SinglePixelPackedSampleModel)(raster.getSampleModel())).getScanlineStride(); if (isDoubleGrid2D) for(int x=sx;x<ex;x++) for(int y=sy;y<ey;y++) { if((x&1)==0) { // btw, setting each pixel separately is faster // than setting a 1x2 pixel grid with setDataElements! load = map.getRGB(doubleField[x][y]); _dbuffer.setElem((2*y)*scanlineStride + x, load); _dbuffer.setElem((2*y+1)*scanlineStride + x, load); // load[0] = map.getRGB(doubleField[x][y]); // _raster.setDataElements(x,2*y,load); // _raster.setDataElements(x,2*y+1,load); } else { load = map.getRGB(doubleField[x][y]); _dbuffer.setElem((2*y+1)*scanlineStride + x, load); _dbuffer.setElem((2*y+2)*scanlineStride + x, load); // load[0] = map.getRGB(doubleField[x][y]); // _raster.setDataElements(x,2*y+1,load); // _raster.setDataElements(x,2*y+2,load); } } else for(int x=sx;x<ex;x++) for(int y=sy;y<ey;y++) { if((x&1)==0) { load = map.getRGB(intField[x][y]); _dbuffer.setElem((2*y)*scanlineStride + x, load); _dbuffer.setElem((2*y+1)*scanlineStride + x, load); // load[0] = map.getRGB(intField[x][y]); // _raster.setDataElements(x,2*y,load); // _raster.setDataElements(x,2*y+1,load); } else { load = map.getRGB(intField[x][y]); _dbuffer.setElem((2*y+1)*scanlineStride + x, load); _dbuffer.setElem((2*y+2)*scanlineStride + x, load); // load[0] = map.getRGB(intField[x][y]); // _raster.setDataElements(x,2*y+1,load); // _raster.setDataElements(x,2*y+2,load); } } } // MacOS X 10.3 Panther has a bug which resets the clip, YUCK // graphics.setClip(clip); graphics.drawImage(buffer, (int)(info.draw.x+translateWidth), (int)info.draw.y, (int)(maxX*scaleWidth), (int)info.draw.height,null); } else if (!info.precise) { buffer = null; // GC the buffer in case the user had changed his mind if (endx > maxX) endx = maxX; if (endy > maxY) endy = maxY; if( startx < 0 ) startx = 0; if( starty < 0 ) starty = 0; final int ex = endx; final int ey = endy; final int sx = startx; final int sy = starty; int _x = 0; int _y = 0; int _width = 0; int _height = 0; // locals are faster... final ColorMap map = this.map; final double infodrawx = info.draw.x; final double infodrawy = info.draw.y; // 1.3.1 doesn't hoist -- does 1.4.1? if (isDoubleGrid2D) for(int x=sx;x<ex;x++) for(int y=sy;y<ey;y++) { final Color c = map.getColor(doubleField[x][y]); if (c.getAlpha() == 0) continue; _x = (int)(translateWidth + infodrawx + scaleWidth * x); _y = (int)(infodrawy + (yScale) * ((x&1)==0?2*y:2*y+1)); _width = (int)(translateWidth + infodrawx + scaleWidth * (x+1)) - _x; _height = (int)(infodrawy + (yScale) * ((x&1)==0?2*y+2:2*y+3)) - _y; // draw // MacOS X 10.3 Panther has a bug which resets the clip, YUCK // graphics.setClip(clip); if( graphics!=null ) { graphics.setColor(c); graphics.fillRect(_x,_y,_width,_height); } else { if( info.clip.intersects(_x,_y,_width,_height) ) putInHere.add(getWrapper((doubleField[x][y]), new Int2D(x, y))); } } else for(int x=sx;x<ex;x++) for(int y=sy;y<ey;y++) { final Color c = map.getColor(intField[x][y]); if (c.getAlpha() == 0) continue; _x = (int)(translateWidth + infodrawx + scaleWidth * x); _y = (int)(infodrawy + (yScale) * ((x&1)==0?2*y:2*y+1)); _width = (int)(translateWidth + infodrawx + scaleWidth * (x+1)) - _x; _height = (int)(infodrawy + (yScale) * ((x&1)==0?2*y+2:2*y+3)) - _y; // draw // MacOS X 10.3 Panther has a bug which resets the clip, YUCK // graphics.setClip(clip); if( graphics!=null ) { graphics.setColor(c); graphics.fillRect(_x,_y,_width,_height); } else { if( info.clip.intersects(_x,_y,_width,_height) ) putInHere.add(getWrapper((intField[x][y]), new Int2D(x, y))); } } } else // precise { graphics.setStroke(new BasicStroke(0.0f)); Rectangle2D.Double preciseRectangle = new Rectangle2D.Double(); buffer = null; // GC the buffer in case the user had changed his mind if (endxd > maxX) endxd = maxX; if (endyd > maxY) endyd = maxY; if( startxd < 0 ) startxd = 0; if( startyd < 0 ) startyd = 0; double _x = 0; double _y = 0; double _width = 0; double _height = 0; // locals are faster... final ColorMap map = this.map; final double infodrawx = info.draw.x; final double infodrawy = info.draw.y; // 1.3.1 doesn't hoist -- does 1.4.1? if (isDoubleGrid2D) for(double x=startxd;x<endxd;x++) for(double y=startyd;y<endyd;y++) { final Color c = map.getColor(doubleField[(int)x][(int)y]); if (c.getAlpha() == 0) continue; graphics.setColor(c); _x = (translateWidth + infodrawx + scaleWidth * x); _y = (infodrawy + (yScale) * ((((int)x)&1)==0?2*y:2*y+1)); _width = (translateWidth + infodrawx + scaleWidth * (x+1)) - _x; _height = (infodrawy + (yScale) * ((((int)x)&1)==0?2*y+2:2*y+3)) - _y; preciseRectangle.setFrame(_x, _y, _width, _height); graphics.fill(preciseRectangle); graphics.draw(preciseRectangle); } else for(double x=startxd;x<endxd;x++) for(double y=startyd;y<endyd;y++) { final Color c = map.getColor(intField[(int)x][(int)y]); if (c.getAlpha() == 0) continue; graphics.setColor(c); _x = (translateWidth + infodrawx + scaleWidth * x); _y = (infodrawy + (yScale) * ((((int)x)&1)==0?2*y:2*y+1)); _width = (translateWidth + infodrawx + scaleWidth * (x+1)) - _x; _height = (infodrawy + (yScale) * ((((int)x)&1)==0?2*y+2:2*y+3)) - _y; preciseRectangle.setFrame(_x, _y, _width, _height); graphics.fill(preciseRectangle); graphics.draw(preciseRectangle); } } // finally, clear dirty flag if we've just drawn (don't clear if we're doing hit testing) if (graphics!=null) setDirtyField(false); } }