package org.geogebra.common.euclidian;
import java.util.ArrayList;
import org.geogebra.common.awt.GAffineTransform;
import org.geogebra.common.awt.GGeneralPath;
import org.geogebra.common.awt.GPathIterator;
import org.geogebra.common.awt.GPoint2D;
import org.geogebra.common.awt.GRectangle;
import org.geogebra.common.awt.GRectangle2D;
import org.geogebra.common.awt.GShape;
import org.geogebra.common.euclidian.clipping.ClipLine;
import org.geogebra.common.factories.AwtFactory;
import org.geogebra.common.kernel.MyPoint;
import org.geogebra.common.kernel.SegmentType;
import org.geogebra.common.util.MyMath;
import org.geogebra.common.util.debug.Log;
/**
* A GeneralPath implementation that does clipping of line segments at the
* screen in double coordinates. This is important to avoid rendering problems
* that occur with GeneralPath when coordinates are larger than Float.MAX_VALUE.
*
* @author Markus Hohenwarter
* @version October 2009
*/
public class GeneralPathClipped implements GShape {
private static final double MAX_COORD_VALUE = 10000;
private ArrayList<MyPoint> pathPoints;
private GGeneralPath gp;
/** view */
protected EuclidianViewInterfaceSlim view;
private double largestCoord;
private boolean needClosePath;
private GRectangle bounds;
/**
* Creates new clipped general path
*
* @param view
* view
*/
public GeneralPathClipped(EuclidianViewInterfaceSlim view) {
// this.view = (EuclidianView)view;
this.view = view;
pathPoints = new ArrayList<MyPoint>();
gp = AwtFactory.getPrototype().newGeneralPath();
// bounds = new Rectangle();
reset();
}
/**
* @return first point of the path
*/
public MyPoint firstPoint() {
if (pathPoints.size() == 0) {
return null;
}
return pathPoints.get(0);
}
/**
* Clears all points and resets internal variables
*/
final public void reset() {
pathPoints.clear();
gp.reset();
// bounds.setBounds(0,0,0,0);
bounds = null;
largestCoord = 0;
needClosePath = false;
}
/**
* Closes path
*/
final public void closePath() {
needClosePath = true;
}
/**
* @return this as GeneralPath
*/
public GGeneralPath getGeneralPath() {
if (pathPoints.size() == 0) {
return gp;
}
gp.reset();
if (largestCoord < MAX_COORD_VALUE) {
addSimpleSegments();
} else {
addClippedSegments();
}
// clear pathPoints to free up memory
pathPoints.clear();
return gp;
}
private void addSimpleSegments() {
int size = pathPoints.size();
// double comparison for GGB-975
for (int i = 0; i < size && i < pathPoints.size(); i++) {
MyPoint curP = pathPoints.get(i);
/// https://play.google.com/apps/publish/?dev_acc=05873811091523087820#ErrorClusterDetailsPlace:p=org.geogebra.android&et=CRASH&lr=LAST_7_DAYS&ecn=java.lang.NullPointerException&tf=SourceFile&tc=org.geogebra.common.euclidian.GeneralPathClipped&tm=addSimpleSegments&nid&an&c&s=new_status_desc
if (curP != null) {
addToGeneralPath(curP, curP.getSegmentType());
} else {
Log.error("curP shouldn't be null here");
}
}
if (needClosePath) {
gp.closePath();
}
}
/**
* Clip all segments at screen to make sure we don't have to render huge
* coordinates. This is especially important for fill the GeneralPath.
*/
private void addClippedSegments() {
GRectangle viewRect = AwtFactory.getPrototype().newRectangle(0, 0,
view.getWidth(), view.getHeight());
MyPoint curP = null, prevP;
int size = pathPoints.size();
// GGB-975: under unknown conditions pathPoints may shrink so we need
// double comparison
for (int i = 0; i < size && i < pathPoints.size(); i++) {
prevP = curP;
curP = pathPoints.get(i);
if (!curP.getLineTo() || prevP == null) {
// moveTo point, make sure it is only slightly outside screen
GPoint2D p = getPointCloseToScreen(curP.getX(), curP.getY());
addToGeneralPath(p, SegmentType.MOVE_TO);
} else {
// clip line at screen
addClippedLine(prevP, curP, viewRect);
}
}
if (needClosePath) {
// line from last point to first point
addClippedLine(curP, pathPoints.get(0), viewRect);
gp.closePath();
}
}
private void addClippedLine(MyPoint prevP, MyPoint curP,
GRectangle viewRect) {
// check if both points on screen
if (viewRect.contains(prevP) && viewRect.contains(curP)) {
// draw line to point
addToGeneralPath(curP, SegmentType.LINE_TO);
return;
}
// at least one point is not on screen: clip line at screen
GPoint2D[] clippedPoints = ClipLine.getClipped(prevP.getX(),
prevP.getY(), curP.getX(), curP.getY(), -10,
view.getWidth() + 10, -10, view.getHeight() + 10);
if (clippedPoints != null) {
// we have two intersection points with the screen
// get closest clip point to prevP
int first = 0;
int second = 1;
if (clippedPoints[first].distance(prevP.getX(),
prevP.getY()) > clippedPoints[second].distance(prevP.getX(),
prevP.getY())) {
first = 1;
second = 0;
}
// draw line to first clip point
addToGeneralPath(clippedPoints[first], SegmentType.LINE_TO);
// draw line between clip points: this ensures high quality
// rendering
// which Java2D doesn't deliver with the regular float GeneralPath
// and huge coords
addToGeneralPath(clippedPoints[second], SegmentType.LINE_TO);
// draw line to end point if not already there
addToGeneralPath(getPointCloseToScreen(curP.getX(), curP.getY()),
SegmentType.LINE_TO);
} else {
// line is off screen
// draw line to off screen end point
addToGeneralPath(getPointCloseToScreen(curP.getX(), curP.getY()),
SegmentType.LINE_TO);
}
}
private GPoint2D getPointCloseToScreen(double ptx, double pty) {
double x = ptx;
double y = pty;
double border = 10;
double right = view.getWidth() + border;
double bottom = view.getHeight() + border;
if (x > right) {
x = right;
} else if (x < -border) {
x = -border;
}
if (y > bottom) {
y = bottom;
} else if (y < -border) {
y = -border;
}
return AwtFactory.getPrototype().newPoint2D(x, y);
}
private double auxX;
private double auxY;
// first control point
private double cont1X = Double.NaN;
private double cont1Y = Double.NaN;
// second control point
private double cont2X = Double.NaN;
private double cont2Y = Double.NaN;
private void addToGeneralPath(GPoint2D q, SegmentType lineTo) {
GPoint2D p = gp.getCurrentPoint();
/*
* We don't need to check the distance, since it has been already
* checked when gp was constructed. Anyway, the distance check is not
* enough here: we also would need to check if this is really a new
* point or just a single point in the same position when a
* moveTo-lineTo-moveTo construct was done.
*/
// boolean distant = true;
// if (p != null) {
// distant = p.distance(q) >= TOLERANCE;
// }
// if (!distant) {
// return;
// }
if (lineTo == SegmentType.CONTROL) {
if (Double.isNaN(cont1X) && Double.isNaN(cont1Y)) {
cont1X = q.getX();
cont1Y = q.getY();
} else {
cont2X = q.getX();
cont2Y = q.getY();
}
} else if (lineTo == SegmentType.CURVE_TO) {
if (!Double.isNaN(cont1X) && !Double.isNaN(cont1Y)
&& !Double.isNaN(cont2X) && !Double.isNaN(cont2Y)) {
gp.curveTo(cont1X, cont1Y, cont2X, cont2Y, q.getX(), q.getY());
cont1X = Double.NaN;
cont1Y = Double.NaN;
cont2X = Double.NaN;
cont2Y = Double.NaN;
}
}
else if (lineTo == SegmentType.AUXILIARY) {
auxX = q.getX();
auxY = q.getY();
} else if (lineTo == SegmentType.ARC_TO && p != null) {
try {
double dx1 = (auxX - p.getX());
double dy1 = (auxY - p.getY());
double dx2 = (auxX - q.getX());
double dy2 = (auxY - q.getY());
double angle = MyMath.angle(dx1, dy1, dx2, dy2);
double cv = btan(Math.PI - angle) * Math.tan(angle / 2);
gp.curveTo(p.getX() + dx1 * cv, p.getY() + dy1 * cv, q.getX()
+ dx2 * cv, q.getY() + dy2 * cv, q.getX(), q.getY());
} catch (Exception e) {
gp.moveTo(q.getX(), q.getY());
}
}
else if (lineTo == SegmentType.LINE_TO && p != null) {
try {
gp.lineTo(q.getX(), q.getY());
} catch (Exception e) {
gp.moveTo(q.getX(), q.getY());
}
} else {
gp.moveTo(q.getX(), q.getY());
}
}
private static double btan(double angle) {
double increment = angle / 2.0;
return 4.0 / 3.0 * Math.sin(increment) / (1.0 + Math.cos(increment));
}
/**
* Move to (x,y).
*
* @param x
* x-coord
* @param y
* y-coord
*/
final public void moveTo(double x, double y) {
addPoint(x, y, SegmentType.MOVE_TO);
}
/**
* Line to (x,y).
*
* @param x
* x-coord
* @param y
* y-coord
*/
final public void lineTo(double x, double y) {
addPoint(x, y, SegmentType.LINE_TO);
}
/**
* Adds point to point list and keeps track of largest coordinate.
*
* @param pos
* insert position
* @param x
* x-coord
* @param y
* y-coord
*/
final public void addPoint(int pos, double x, double y) {
if (Double.isNaN(y)) {
return;
}
MyPoint p = new MyPoint(x, y, SegmentType.LINE_TO);
updateBounds(p);
pathPoints.ensureCapacity(pos + 1);
while (pathPoints.size() <= pos) {
pathPoints.add(null);
}
pathPoints.set(pos, p);
}
/**
* Adds point to point list and keeps track of largest coordinate.
*
* @param x
* x-coord
* @param y
* y-coord
* @param segmentType
* path segment type
*/
protected final void addPoint(double x, double y, SegmentType segmentType) {
if (Double.isNaN(y)) {
return;
}
MyPoint p = new MyPoint(x, y, segmentType);
updateBounds(p);
pathPoints.add(p);
}
private void updateBounds(MyPoint p) {
if (bounds == null) {
bounds = AwtFactory.getPrototype().newRectangle();
bounds.setBounds((int) p.getX(), (int) p.getY(), 0, 0);
}
if (Math.abs(p.getX()) > largestCoord) {
largestCoord = Math.abs(p.getX());
}
if (Math.abs(p.getY()) > largestCoord) {
largestCoord = Math.abs(p.getY());
}
bounds.add(p.getX(), p.getY());
}
/**
* @return current point
*/
public GPoint2D getCurrentPoint() {
if (pathPoints.size() == 0) {
return null;
}
return pathPoints.get(pathPoints.size() - 1);
}
/**
* Transforms this path
*
* @param af
* transformation
*/
public void transform(GAffineTransform af) {
int size = pathPoints.size();
for (int i = 0; i < size; i++) {
MyPoint p = pathPoints.get(i);
af.transform(p, p);
}
}
/**
* @param p
* point
* @return true if contains given point
*/
public boolean contains(GPoint2D p) {
return getGeneralPath().contains(p);
}
/**
* @param rect
* rectangle
* @return true if contains given rectangle
*/
@Override
public boolean contains(GRectangle2D rect) {
return getGeneralPath().contains(rect);
}
@Override
public boolean contains(double arg0, double arg1) {
return getGeneralPath().contains(arg0, arg1);
}
/**
* @param arg0
* x min
* @param arg1
* y min
* @param arg2
* width
* @param arg3
* height
* @return true if contains rectangle given by args
*/
public boolean contains(double arg0, double arg1, double arg2,
double arg3) {
return getGeneralPath().contains(arg0, arg1, arg2, arg3);
}
@Override
public boolean contains(int x, int y) {
// TODO Auto-generated method stub
return getGeneralPath().contains(x, y);
}
/**
* @param rectangle
* rectangle to be checked
* @return whether rectangle is contained in this path
*/
public boolean contains(GRectangle rectangle) {
// TODO Auto-generated method stub
return getGeneralPath().contains(rectangle);
}
@Override
public GRectangle getBounds() {
return bounds == null ? AwtFactory.getPrototype().newRectangle()
: bounds;
}
@Override
public GRectangle2D getBounds2D() {
return bounds == null ? AwtFactory.getPrototype().newRectangle()
: bounds;
}
/*
* public PathIterator getPathIterator(AffineTransform arg0) { return
* geogebra
* .awt.GeneralPath.getAwtGeneralPath(getGeneralPath()).getPathIterator
* (arg0); }
*/
@Override
public GPathIterator getPathIterator(GAffineTransform arg0) {
return getGeneralPath().getPathIterator(arg0);
}
@Override
public boolean intersects(GRectangle2D arg0) {
return getGeneralPath().intersects(arg0);
}
@Override
public boolean intersects(double arg0, double arg1, double arg2,
double arg3) {
return getGeneralPath().intersects(arg0, arg1, arg2, arg3);
}
@Override
public boolean intersects(int i, int j, int k, int l) {
return getGeneralPath().intersects(i, j, k, l);
}
/**
* @param x
* center x-coord
* @param y
* center y-coord
* @param radius
* inradius of the square
* @return whether this intersects square with center (x,y) and inradius
* radius
*/
public boolean intersects(int x, int y, int radius) {
return getGeneralPath().intersects(x - radius, y - radius, 2 * radius,
2 * radius);
}
/*
* public Shape getAwtShape() { return
* geogebra.awt.GeneralPath.getAwtGeneralPath(getGeneralPath()); }
*/
}