/*
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);
}
}