/*
* 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.model.drawing;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import com.t3.client.AppState;
import com.t3.model.CellPoint;
import com.t3.model.ZonePoint;
import com.t3.xstreamversioned.version.SerializationVersion;
/**
* A drawing tool that will draw a line template between 2 vertices.
*
* @author jgorrell
* @version $Revision: 5967 $ $Date: 2013-06-02 15:05:50 -0400 (Sun, 02 Jun
* 2013) $ $Author: azhrei_fje $
*/
@SerializationVersion(0)
public class LineTemplate extends AbstractTemplate {
/*---------------------------------------------------------------------------------------------
* Instance Variables
*-------------------------------------------------------------------------------------------*/
/**
* Are straight lines drawn double width?
*/
private boolean doubleWide = AppState.useDoubleWideLine();
/**
* This vertex is used to determine the path.
*/
private ZonePoint pathVertex;
/**
* The calculated path for this line.
*/
private ArrayList<CellPoint> path;
/**
* The pool of points.
*/
private ArrayList<CellPoint> pool;
/**
* The line is drawn in this quadrant. A string is used as a hack to get
* around the hessian library's problem w/ serialization of enums
*/
private Quadrant quadrant = null;
/**
* Flag used to determine mouse position relative to vertex position
*/
private boolean mouseSlopeGreater;
/*---------------------------------------------------------------------------------------------
* Overridden AbstractTemplate Methods
*-------------------------------------------------------------------------------------------*/
/**
* @see com.t3.model.drawing.AbstractTemplate#paintArea(java.awt.Graphics2D,
* int, int, int, int, int, int)
*/
@Override
protected void paintArea(Graphics2D g, int x, int y, int xOff, int yOff, int gridSize, int distance) {
paintArea(g, xOff, yOff, gridSize, getQuadrant());
}
/**
* This method is cheating, the distance parameter was replaced with the
* offset into the path.
*
* @see com.t3.model.drawing.AbstractTemplate#paintBorder(java.awt.Graphics2D,
* int, int, int, int, int, int)
*/
@Override
protected void paintBorder(Graphics2D g, int x, int y, int xOff, int yOff, int gridSize, int pElement) {
// Have to scan 3 points behind and ahead, since that is the maximum number of points
// that can be added to the path from any single intersection.
boolean[] noPaint = new boolean[4];
for (int i = pElement - 3; i < pElement + 3; i++) {
if (i < 0 || i >= path.size() || i == pElement)
continue;
CellPoint p = path.get(i);
// Ignore diagonal cells and cells that are not adjacent
int dx = p.x - x;
int dy = p.y - y;
if (Math.abs(dx) == Math.abs(dy) || Math.abs(dx) > 1 || Math.abs(dy) > 1)
continue;
// Remove the border between the 2 points
noPaint[dx != 0 ? (dx < 0 ? 0 : 2) : (dy < 0 ? 3 : 1)] = true;
} // endif
// Paint the borders as needed
if (!noPaint[0])
paintCloseVerticalBorder(g, xOff, yOff, gridSize, getQuadrant());
if (!noPaint[1])
paintFarHorizontalBorder(g, xOff, yOff, gridSize, getQuadrant());
if (!noPaint[2])
paintFarVerticalBorder(g, xOff, yOff, gridSize, getQuadrant());
if (!noPaint[3])
paintCloseHorizontalBorder(g, xOff, yOff, gridSize, getQuadrant());
}
/**
* @see com.t3.model.drawing.AbstractTemplate#paint(java.awt.Graphics2D,
* boolean, boolean)
*/
@Override
protected void paint(Graphics2D g, boolean border, boolean area) {
if (getZoneReference() == null) {
return;
}
// Need to paint? We need a line and to translate the painting
if (pathVertex == null)
return;
if (getRadius() == 0)
return;
if (calcPath() == null)
return;
// Paint each element in the path
int gridSize = getZoneReference().value().getGrid().getSize();
ListIterator<CellPoint> i = path.listIterator();
while (i.hasNext()) {
CellPoint p = i.next();
int xOff = p.x * gridSize;
int yOff = p.y * gridSize;
int distance = getDistance(p.x, p.y);
// Paint what is needed.
if (area) {
paintArea(g, p.x, p.y, xOff, yOff, gridSize, distance);
} // endif
if (border) {
paintBorder(g, p.x, p.y, xOff, yOff, gridSize, i.previousIndex());
} // endif
} // endfor
}
/**
* @see com.t3.model.drawing.AbstractTemplate#setVertex(ZonePoint)
*/
@Override
public void setVertex(ZonePoint vertex) {
clearPath();
super.setVertex(vertex);
}
/**
* @see com.t3.model.drawing.AbstractTemplate#setRadius(int)
*/
@Override
public void setRadius(int squares) {
if (squares == getRadius())
return;
clearPath();
super.setRadius(squares);
}
/*---------------------------------------------------------------------------------------------
* Instance Methods
*-------------------------------------------------------------------------------------------*/
/**
* Calculate the path
*
* @return The new path or <code>null</code> if there is no path.
*/
protected List<CellPoint> calcPath() {
if (getRadius() == 0)
return null;
if (pathVertex == null)
return null;
int radius = getRadius();
// Is there a slope?
ZonePoint vertex = getVertex();
if (vertex.equals(pathVertex))
return null;
int dx = pathVertex.x - vertex.x;
int dy = pathVertex.y - vertex.y;
// Start the line at 0,0
clearPath();
path = new ArrayList<CellPoint>();
path.add(getPointFromPool(0, 0));
MathContext mc = MathContext.DECIMAL128;
MathContext rmc = new MathContext(MathContext.DECIMAL64.getPrecision(), RoundingMode.DOWN);
if (dx != 0 && dy != 0) {
// Calculate quadrant and the slope
setQuadrant((dx < 0) ? (dy < 0 ? Quadrant.NORTH_WEST : Quadrant.SOUTH_WEST) : (dy < 0 ? Quadrant.NORTH_EAST : Quadrant.SOUTH_EAST));
BigDecimal m = BigDecimal.valueOf(dy).divide(BigDecimal.valueOf(dx), mc).abs();
// Find the path
CellPoint p = path.get(path.size() - 1);
while (getDistance(p.x, p.y) <= radius) {
int x = p.x;
int y = p.y;
// Which border does the point exit the cell?
double xValue = BigDecimal.valueOf(y + 1).divide(m, mc).round(rmc).doubleValue();
double yValue = BigDecimal.valueOf(x + 1).multiply(m, mc).round(rmc).doubleValue();
if (xValue == x + 1 && yValue == y + 1) {
// Special case, right on the diagonal
if (doubleWide || !mouseSlopeGreater)
path.add(getPointFromPool(x + 1, y));
if (doubleWide || mouseSlopeGreater)
path.add(getPointFromPool(x, y + 1));
path.add(getPointFromPool(x + 1, y + 1));
} else if (Math.floor(xValue) == x) {
path.add(getPointFromPool(x, y + 1));
} else if (Math.floor(yValue) == y) {
path.add(getPointFromPool(x + 1, y));
} else {
// System.err.println("I can't do math: dx=" + dx + " dy=" + dy + " m=" + m + " x=" + x + " xValue=" + xValue + " y=" + y + " yValue=" + yValue);
return path;
} // endif
p = path.get(path.size() - 1);
} // endwhile
// Clear the last of the pool
if (pool != null) {
pool.clear();
pool = null;
} // endif
} else {
// Straight line
int xInc = dx != 0 ? 1 : 0;
int yInc = dy != 0 ? 1 : 0;
int x = xInc;
int y = yInc;
int xTouch = (dx != 0) ? 0 : -1;
int yTouch = (dy != 0) ? 0 : -1;
if (doubleWide)
path.add(getPointFromPool(xTouch, yTouch));
while (getDistance(x, y) <= radius) {
path.add(getPointFromPool(x, y));
if (doubleWide)
path.add(getPointFromPool(x + xTouch, y + yTouch));
x += xInc;
y += yInc;
} // endwhile
} // endif
return path;
}
/**
* Get a point from the pool or create a new one.
*
* @param x
* The x coordinate of the new point.
* @param y
* The y coordinate of the new point.
* @return The new point.
*/
public CellPoint getPointFromPool(int x, int y) {
CellPoint p = null;
if (pool != null) {
p = pool.remove(pool.size() - 1);
if (pool.isEmpty())
pool = null;
} // endif
if (p == null) {
p = new CellPoint(0, 0);
} // endif
p.x = x;
p.y = y;
return p;
}
/**
* Add a point back to the pool.
*
* @param p
* Add this point back
*/
public void addPointToPool(CellPoint p) {
if (pool != null)
pool.add(p);
}
/**
* Get the pathVertex for this LineTemplate.
*
* @return Returns the current value of pathVertex.
*/
public ZonePoint getPathVertex() {
return pathVertex;
}
/**
* Set the value of pathVertex for this LineTemplate.
*
* @param pathVertex
* The pathVertex to set.
*/
public void setPathVertex(ZonePoint pathVertex) {
if (pathVertex.equals(this.pathVertex))
return;
clearPath();
this.pathVertex = pathVertex;
}
/**
* Clear the current path. This will cause it to be recalculated during the
* next draw.
*/
public void clearPath() {
if (path != null)
pool = path;
path = null;
}
/**
* Get the quadrant for this LineTemplate.
*
* @return Returns the current value of quadrant.
*/
public Quadrant getQuadrant() {
return quadrant;
}
/**
* Set the value of quadrant for this LineTemplate.
*
* @param quadrant
* The quadrant to set.
*/
public void setQuadrant(Quadrant quadrant) {
this.quadrant = quadrant;
}
/**
* Get the mouseSlopeGreater for this LineTemplate.
*
* @return Returns the current value of mouseSlopeGreater.
*/
public boolean isMouseSlopeGreater() {
return mouseSlopeGreater;
}
/**
* Set the value of mouseSlopeGreater for this LineTemplate.
*
* @param aMouseSlopeGreater
* The mouseSlopeGreater to set.
*/
public void setMouseSlopeGreater(boolean aMouseSlopeGreater) {
mouseSlopeGreater = aMouseSlopeGreater;
}
/**
* Get the doubleWide for this LineTemplate.
*
* @return Returns the current value of doubleWide.
*/
public boolean isDoubleWide() {
return doubleWide;
}
/**
* Set the value of doubleWide for this LineTemplate.
*
* @param aDoubleWide
* The doubleWide to set.
*/
public void setDoubleWide(boolean aDoubleWide) {
doubleWide = aDoubleWide;
}
/** @return Getter for path */
public ArrayList<CellPoint> getPath() {
return path;
}
/**
* @param path
* Setter for the path to set
*/
public void setPath(ArrayList<CellPoint> path) {
this.path = path;
}
/*---------------------------------------------------------------------------------------------
* Drawable Interface Methods
*-------------------------------------------------------------------------------------------*/
/**
* @see com.t3.model.drawing.Drawable#getBounds()
*/
@Override
public Rectangle getBounds() {
// Get all of the numbers needed for the calculation
if (getZoneReference() == null) {
return new Rectangle();
}
int gridSize = getZoneReference().value().getGrid().getSize();
ZonePoint vertex = getVertex();
// Find the point that is farthest away in the path, then adjust
ZonePoint minp = null;
ZonePoint maxp = null;
if (path == null) {
calcPath();
if (path == null) {
// If the calculated path is still null, then the line is invalid.
return new Rectangle();
}
}
for (CellPoint pt : path) {
ZonePoint p = getZoneReference().value().getGrid().convert(pt);
p = new ZonePoint(vertex.x + p.x, vertex.y + p.y);
if (minp == null) {
minp = new ZonePoint(p.x, p.y);
maxp = new ZonePoint(p.x, p.y);
}
minp.x = Math.min(minp.x, p.x);
minp.y = Math.min(minp.y, p.y);
maxp.x = Math.max(maxp.x, p.x);
maxp.y = Math.max(maxp.y, p.y);
}
maxp.x += gridSize;
maxp.y += gridSize;
// The path is only calculated for the south-east quadrant, so
// appropriately reflect the bounding box around the starting vertex.
if (getXMult(getQuadrant()) < 0) {
int tmp;
tmp = vertex.x - (maxp.x - vertex.x);
maxp.x = vertex.x - (minp.x - vertex.x);
minp.x = tmp;
}
if (getYMult(getQuadrant()) < 0) {
int tmp;
tmp = vertex.y - (maxp.y - vertex.y);
maxp.y = vertex.y - (minp.y - vertex.y);
minp.y = tmp;
}
int width = (maxp.x - minp.x);
int height = (maxp.y - minp.y);
// Account for pen size
// We don't really know what the pen size will be, so give a very rough
// overestimate
// We'll have to figure this out someday
minp.x -= 10;
minp.y -= 10;
width += 20;
height += 20;
return new Rectangle(minp.x, minp.y, width, height);
}
@Override
public Area getArea() {
if (path == null) {
calcPath();
if (path == null) {
// If the calculated path is still null, then the line is invalid and should be deleted.
return new Area();
}
}
// I don't feel like figuring out the exact shape of this right now
return null;
}
}