/*
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.field.grid.*;
import sim.portrayal.*;
import sim.portrayal.simple.*;
import sim.util.*;
import java.awt.*;
import java.util.*;
import java.awt.geom.*;
import sim.display.*;
/**
Portrayal for hexagonal grids (each cell has six equally-distanced neighbors). It can draw
either continuous and descrete sparse fields.
The 'location' passed
into the DrawInfo2D handed to the SimplePortryal2D is an Int2D.
*/
public class HexaSparseGridPortrayal2D extends SparseGridPortrayal2D
{
int[] xPoints = new int[6];
int[] yPoints = new int[6];
double[] xyC = new double[2];
double[] xyC_ul = new double[2];
double[] xyC_up = new double[2];
double[] xyC_ur = new double[2];
final static void getxyC( final int x, final int y, final double xScale, final double yScale, final double tx, final double ty, final double[] xyC )
{
xyC[0] = tx + xScale * (1.5 * x + 1);
xyC[1] = ty + yScale * (1.0 + 2.0 * y + (x<0?(-x)%2:x%2) );
}
public HexaSparseGridPortrayal2D()
{
super();
defaultPortrayal = new HexagonalPortrayal2D();
}
/** @deprecated Use setDrawPolicy. */
public HexaSparseGridPortrayal2D (DrawPolicy policy)
{
super(policy);
defaultPortrayal = new HexagonalPortrayal2D();
}
/** The ratio of the width of a hexagon to its height: 1 / Sin(60 degrees), otherwise known as 2 / Sqrt(3) */
static final double HEXAGONAL_RATIO = 2/Math.sqrt(3);
public void setObjectLocation(Object object, Object location, GUIState gui)
{
synchronized(gui.state.schedule)
{
if (location != null)
{
if (location instanceof Int2D)
{
Int2D loc = (Int2D) location;
if (object instanceof Fixed2D && (!((Fixed2D)object).maySetLocation(field, loc)))
return; // this is deprecated and will be deleted
else if (object instanceof Constrained)
loc = (Int2D)((Constrained)object).constrainLocation(field, loc);
if (loc != null)
((SparseGrid2D)field).setObjectLocation(object, loc);
}
}
}
}
/*
public void setObjectPosition(Object object, Point2D.Double position, DrawInfo2D fieldPortrayalInfo)
{
final SparseGrid2D field = (SparseGrid2D)this.field;
if (field==null) return;
if (field.getObjectLocation(object) == null) return;
Int2D location = (Int2D)(getPositionLocation(position, fieldPortrayalInfo));
if (location != null)
{
if (object instanceof Fixed2D && (!((Fixed2D)object).maySetLocation(field, location)))
return; // this is deprecated and will be deleted
else if (object instanceof Constrained)
location = (Int2D)((Constrained)object).constrainLocation(field, location);
if (location != null)
field.setObjectLocation(object, location);
}
}
*/
public Double2D getScale(DrawInfo2D info)
{
synchronized(info.gui.state.schedule)
{
final Grid2D field = (Grid2D) this.field;
if (field==null) return null;
int maxX = field.getWidth();
int maxY = field.getHeight();
if (maxX == 0 || maxY == 0) return null;
final double divideByX = ((maxX%2==0)?(3.0*maxX/2.0+0.5):(3.0*maxX/2.0+2.0));
final double divideByY = (1.0+2.0*maxY);
final double xScale = info.draw.width / divideByX;
final double yScale = info.draw.height / divideByY;
return new Double2D(xScale, yScale);
}
}
public Object getPositionLocation(Point2D.Double position, DrawInfo2D info)
{
Double2D scale = getScale(info);
double xScale = scale.x;
double yScale = scale.y;
int startx = (int)Math.floor(((position.getX() - info.draw.x)/xScale-0.5)/1.5);
int starty = (int)Math.floor((position.getY() - info.draw.y)/(yScale*2.0));
return new Int2D(startx, starty);
}
public Point2D.Double getLocationPosition(Object location, DrawInfo2D info)
{
synchronized(info.gui.state.schedule)
{
final Grid2D field = (Grid2D) this.field;
if (field==null) return null;
int maxX = field.getWidth();
int maxY = field.getHeight();
if (maxX == 0 || maxY == 0) return null;
final double divideByX = ((maxX%2==0)?(3.0*maxX/2.0+0.5):(3.0*maxX/2.0+2.0));
final double divideByY = (1.0+2.0*maxY);
final double xScale = info.draw.width / divideByX;
final double yScale = info.draw.height / divideByY;
//int startx = (int)Math.floor(((info.clip.x - info.draw.x)/xScale-0.5)/1.5)-2;
//int starty = (int)Math.floor((info.clip.y - info.draw.y)/(yScale*2.0))-2;
//int endx = /*startx +*/ (int)Math.floor(((info.clip.x - info.draw.x + info.clip.width)/xScale-0.5)/1.5) + 4; // with rounding, width be as much as 1 off
//int endy = /*starty +*/ (int)Math.floor((info.clip.y - info.draw.y + info.clip.height)/(yScale*2.0)) + 4; // with rounding, height be as much as 1 off
DrawInfo2D newinfo = new DrawInfo2D(info.gui, info.fieldPortrayal, new Rectangle2D.Double(0,0,
Math.ceil(info.draw.width / (HEXAGONAL_RATIO * ((maxX - 1) * 3.0 / 4.0 + 1))),
Math.ceil(info.draw.height / (maxY + 0.5))),
info.clip/*, xPoints, yPoints*/, info); // we don't do further clipping
newinfo.precise = info.precise;
Int2D loc = (Int2D) location;
if (loc == null) return null;
final int x = loc.x;
final int y = loc.y;
getxyC( x, y, xScale, yScale, info.draw.x, info.draw.y, xyC );
getxyC( field.ulx(x,y), field.uly(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_ul );
getxyC( field.upx(x,y), field.upy(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_up );
getxyC( field.urx(x,y), field.ury(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_ur );
xPoints[0] = (int)Math.floor(xyC_ur[0]-0.5*xScale);
//yPoints[0] = (int)Math.floor(xyC_ur[1]+yScale);
//xPoints[1] = (int)Math.floor(xyC_up[0]+0.5*xScale);
yPoints[1] = (int)Math.floor(xyC_up[1]+yScale);
//xPoints[2] = (int)Math.floor(xyC_up[0]-0.5*xScale);
//yPoints[2] = (int)Math.floor(xyC_up[1]+yScale);
xPoints[3] = (int)Math.floor(xyC_ul[0]+0.5*xScale);
//yPoints[3] = (int)Math.floor(xyC_ul[1]+yScale);
//xPoints[4] = (int)Math.floor(xyC[0]-0.5*xScale);
yPoints[4] = (int)Math.floor(xyC[1]+yScale);
//xPoints[5] = (int)Math.floor(xyC[0]+0.5*xScale);
//yPoints[5] = (int)Math.floor(xyC[1]+yScale);
// compute the width of the object -- we tried computing the EXACT width each time, but
// it results in weird-shaped circles etc, so instead we precomputed a standard width
// and height, and just compute the x values here.
newinfo.draw.x = xPoints[3];
newinfo.draw.y = yPoints[1];
// adjust drawX and drawY to center
newinfo.draw.x +=(xPoints[0]-xPoints[3]) / 2.0;
newinfo.draw.y += (yPoints[4]-yPoints[1]) / 2.0;
return new Point2D.Double(newinfo.draw.x, newinfo.draw.y);
}
}
protected void hitOrDraw(Graphics2D graphics, DrawInfo2D info, Bag putInHere)
{
final SparseGrid2D field = (SparseGrid2D) this.field;
if (field==null) return;
boolean objectSelected = !selectedWrappers.isEmpty();
int maxX = field.getWidth();
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 divideByY = (1.0+2.0*maxY);
final double xScale = info.draw.width / divideByX;
final double yScale = info.draw.height / divideByY;
int startx = (int)Math.floor(((info.clip.x - info.draw.x)/xScale-0.5)/1.5)-2;
int starty = (int)Math.floor((info.clip.y - info.draw.y)/(yScale*2.0))-2;
int endx = /*startx +*/ (int)Math.floor(((info.clip.x - info.draw.x + info.clip.width)/xScale-0.5)/1.5) + 4; // with rounding, width be as much as 1 off
int endy = /*starty +*/ (int)Math.floor((info.clip.y - info.draw.y + info.clip.height)/(yScale*2.0)) + 4; // with rounding, height be as much as 1 off
// double precomputedWidth = -1; // see discussion further below
// double precomputedHeight = -1; // see discussion further below
//
//
// CAUTION!
//
// At some point we should triple check the math for rounding such
// that the margins are drawn properly
//
//
// Horizontal hexagons are staggered. This complicates computations. Thus
// if you have a M x N grid scaled to SCALE, then
// your height is (N + 0.5) * SCALE
// and your width is ((M - 1) * (3/4) + 1) * HEXAGONAL_RATIO * SCALE
// we invert these calculations here to compute the rough width and height
// for the newinfo here. Additionally, because the original screen sizes were likely
// converted from floats to ints, there's a round down there, so we round up to
// compensate. This usually results in nice circles.
// final Rectangle clip = (graphics==null ? null : graphics.getClipBounds());
DrawInfo2D newinfo = new DrawInfo2D(info.gui, info.fieldPortrayal, new Rectangle2D.Double(0,0,
Math.ceil(info.draw.width / (HEXAGONAL_RATIO * ((maxX - 1) * 3.0 / 4.0 + 1))),
Math.ceil(info.draw.height / (maxY + 0.5))),
info.clip/*, xPoints, yPoints*/, info); // we don't do further clipping
newinfo.precise = info.precise;
newinfo.fieldPortrayal = this;
// If the person has specified a policy, we have to iterate through the
// bags. At present we have to do this by using a hash table iterator
// (yuck -- possibly expensive, have to search through empty locations).
//
// We never use the policy to determine hitting. hence this only works if graphics != null
if (policy != null && graphics != null)
{
Bag policyBag = new Bag();
Iterator iterator = field.locationBagIterator();
while(iterator.hasNext())
{
Bag objects = (Bag)(iterator.next());
// restrict the number of objects to draw
policyBag.clear(); // fast
if (policy.objectToDraw(objects,policyBag)) // if this function returns FALSE, we should use objects as is, else use the policy bag.
objects = policyBag; // returned TRUE, so we're going to use the modified policyBag instead.
// draw 'em
for(int xO=0;xO<objects.numObjs;xO++)
{
final Object portrayedObject = objects.objs[xO];
Int2D loc = field.getObjectLocation(portrayedObject);
final int x = loc.x;
final int y = loc.y;
// here we only draw the object if it's within our range. However objects
// might leak over to other places, so I dunno... I give them the benefit
// of the doubt that they might be three times the size they oughta be, hence the -2 and +2's
if (loc.x >= startx -2 && loc.x < endx + 4 &&
loc.y >= starty -2 && loc.y < endy + 4)
{
Portrayal p = getPortrayalForObject(portrayedObject);
if (!(p instanceof SimplePortrayal2D))
throw new RuntimeException("Unexpected Portrayal " + p + " for object " +
portrayedObject + " -- expected a SimplePortrayal2D");
SimplePortrayal2D portrayal = (SimplePortrayal2D) p;
getxyC( x, y, xScale, yScale, info.draw.x, info.draw.y, xyC );
getxyC( field.ulx(x,y), field.uly(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_ul );
getxyC( field.upx(x,y), field.upy(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_up );
getxyC( field.urx(x,y), field.ury(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_ur );
xPoints[0] = (int)Math.floor(xyC_ur[0]-0.5*xScale);
//yPoints[0] = (int)Math.floor(xyC_ur[1]+yScale);
//xPoints[1] = (int)Math.floor(xyC_up[0]+0.5*xScale);
yPoints[1] = (int)Math.floor(xyC_up[1]+yScale);
//xPoints[2] = (int)Math.floor(xyC_up[0]-0.5*xScale);
//yPoints[2] = (int)Math.floor(xyC_up[1]+yScale);
xPoints[3] = (int)Math.floor(xyC_ul[0]+0.5*xScale);
//yPoints[3] = (int)Math.floor(xyC_ul[1]+yScale);
//xPoints[4] = (int)Math.floor(xyC[0]-0.5*xScale);
yPoints[4] = (int)Math.floor(xyC[1]+yScale);
//xPoints[5] = (int)Math.floor(xyC[0]+0.5*xScale);
//yPoints[5] = (int)Math.floor(xyC[1]+yScale);
// compute the width of the object -- we tried computing the EXACT width each time, but
// it results in weird-shaped circles etc, so instead we precomputed a standard width
// and height, and just compute the x values here.
newinfo.draw.x = xPoints[3];
newinfo.draw.y = yPoints[1];
// adjust drawX and drawY to center
newinfo.draw.x +=(xPoints[0]-xPoints[3]) / 2.0;
newinfo.draw.y += (yPoints[4]-yPoints[1]) / 2.0;
newinfo.location = loc;
// we never use this policy for hitting -- see above
//if (graphics == null)
// {
// if (portrayal.hitObject(portrayedObject, newinfo))
// putInHere.add(getWrapper(portrayedObject, newinfo.gui));
// }
//else
{
// MacOS X 10.3 Panther has a bug which resets the clip, YUCK
// graphics.setClip(clip);
newinfo.selected = (objectSelected && // there's something there
selectedWrappers.get(portrayedObject) != null);
/* {
LocationWrapper wrapper = (LocationWrapper)(selectedWrappers.get(portrayedObject));
portrayal.setSelected(wrapper,true);
portrayal.draw(portrayedObject, graphics, newinfo);
portrayal.setSelected(wrapper,false);
}
else */ portrayal.draw(portrayedObject, graphics, newinfo);
}
}
}
}
}
else // the easy way -- draw the objects one by one
{
Bag objects = field.getAllObjects();
for(int xO=0;xO<objects.numObjs;xO++)
{
final Object portrayedObject = objects.objs[xO];
Int2D loc = field.getObjectLocation(portrayedObject);
final int x = loc.x;
final int y = loc.y;
// here we only draw the object if it's within our range. However objects
// might leak over to other places, so I dunno... I give them the benefit
// of the doubt that they might be three times the size they oughta be, hence the -2 and +2's
if (loc.x >= startx -2 && loc.x < endx + 4 &&
loc.y >= starty -2 && loc.y < endy + 4)
{
Portrayal p = getPortrayalForObject(portrayedObject);
if (!(p instanceof SimplePortrayal2D))
throw new RuntimeException("Unexpected Portrayal " + p + " for object " +
portrayedObject + " -- expected a SimplePortrayal2D");
SimplePortrayal2D portrayal = (SimplePortrayal2D) p;
getxyC( x, y, xScale, yScale, info.draw.x, info.draw.y, xyC );
getxyC( field.ulx(x,y), field.uly(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_ul );
getxyC( field.upx(x,y), field.upy(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_up );
getxyC( field.urx(x,y), field.ury(x,y), xScale, yScale, info.draw.x, info.draw.y, xyC_ur );
xPoints[0] = (int)Math.floor(xyC_ur[0]-0.5*xScale);
//yPoints[0] = (int)Math.floor(xyC_ur[1]+yScale);
//xPoints[1] = (int)Math.floor(xyC_up[0]+0.5*xScale);
yPoints[1] = (int)Math.floor(xyC_up[1]+yScale);
//xPoints[2] = (int)Math.floor(xyC_up[0]-0.5*xScale);
//yPoints[2] = (int)Math.floor(xyC_up[1]+yScale);
xPoints[3] = (int)Math.floor(xyC_ul[0]+0.5*xScale);
//yPoints[3] = (int)Math.floor(xyC_ul[1]+yScale);
//xPoints[4] = (int)Math.floor(xyC[0]-0.5*xScale);
yPoints[4] = (int)Math.floor(xyC[1]+yScale);
//xPoints[5] = (int)Math.floor(xyC[0]+0.5*xScale);
//yPoints[5] = (int)Math.floor(xyC[1]+yScale);
// compute the width of the object -- we tried computing the EXACT width each time, but
// it results in weird-shaped circles etc, so instead we precomputed a standard width
// and height, and just compute the x values here.
newinfo.draw.x = xPoints[3];
newinfo.draw.y = yPoints[1];
// adjust drawX and drawY to center
newinfo.draw.x +=(xPoints[0]-xPoints[3]) / 2.0;
newinfo.draw.y += (yPoints[4]-yPoints[1]) / 2.0;
if (graphics == null)
{
if (portrayal.hitObject(portrayedObject, newinfo))
putInHere.add(getWrapper(portrayedObject, newinfo.gui));
}
else
{
// MacOS X 10.3 Panther has a bug which resets the clip, YUCK
// graphics.setClip(clip);
newinfo.selected = (objectSelected && // there's something there
selectedWrappers.get(portrayedObject) != null);
/* {
LocationWrapper wrapper = (LocationWrapper)(selectedWrappers.get(portrayedObject));
portrayal.setSelected(wrapper,true);
portrayal.draw(portrayedObject, graphics, newinfo);
portrayal.setSelected(wrapper,false);
}
else */portrayal.draw(portrayedObject, graphics, newinfo);
}
}
}
}
}
/** This is not supported by hexagonal portrayals. Throws an exception. */
public void setBorder(boolean on) { throw new RuntimeException("Border drawing is not supported by hexagonal portrayals."); }
/** This is not supported by hexagonal portrayals. Throws an exception. */
public void setGridLines(boolean on) { throw new RuntimeException("Grid line drawing is not supported by hexagonal portrayals."); }
}