package net.sourceforge.fidocadj.primitives;
import java.io.*;
import java.util.*;
import net.sourceforge.fidocadj.dialogs.*;
import net.sourceforge.fidocadj.export.*;
import net.sourceforge.fidocadj.geom.*;
import net.sourceforge.fidocadj.globals.*;
import net.sourceforge.fidocadj.graphic.*;
/** Class to handle the ComplexCurve primitive.
<pre>
This file is part of FidoCadJ.
FidoCadJ is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FidoCadJ is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FidoCadJ. If not,
@see <a href=http://www.gnu.org/licenses/>http://www.gnu.org/licenses/</a>.
Copyright 2011-2017 by Davide Bucci
Spline calculations by Tim Lambert
http://www.cse.unsw.edu.au/~lambert/splines/
</pre>
@author Davide Bucci
*/
public final class PrimitiveComplexCurve
extends GraphicPrimitive
{
private int nPoints;
private boolean isFilled;
private boolean isClosed;
private final Arrow arrowData;
private int dashStyle;
// The natural spline is drawn as a polygon. Even if this is a rather
// crude technique, it fits well with the existing architecture (in
// particular for the export facilities), since everything that it is
// needed for a polygon is available and can be reused here.
// In some cases (for example for drawing), a ShapeInterface is created,
// since it gives better results than a polygon.
// A first polygon stored in screen coordinates.
private PolygonInterface p;
// A second polygon stored in logical coordinates.
private PolygonInterface q;
// 5 points is the initial storage size, which is increased if needed.
// In other words, we initially create space for storing 5 points and
// we increase that if needed.
int storageSize=5;
static final int STEPS=24;
// Some stored data
private int xmin, ymin;
private int width, height;
// Those are data which are kept for the fast redraw of this primitive.
// Basically, they are calculated once and then used as much as possible
// without having to calculate everything from scratch.
private float w;
private ShapeInterface gp;
/** Gets the number of control points used.
@return the number of points used by the primitive.
*/
public int getControlPointNumber()
{
return nPoints+2;
}
/** Constructor.
Create a ComplexCurve. Add points with the addPoint method.
@param f the name of the font for attached text.
@param size the size of the font for attached text.
*/
public PrimitiveComplexCurve(String f, int size)
{
super();
arrowData=new Arrow();
isFilled=false;
nPoints=0;
p = null;
initPrimitive(storageSize, f, size);
}
/** Create a ComplexCurve. Add points with the addPoint method.
@param f specifies if the ComplexCurve should be filled.
@param c specifies if the ComplexCurve should be closed.
@param layer the layer to be used.
@param arrowS arrow to be drawn at the beginning of the curve.
@param arrowE arrow to be drawn at the beginning of the curve.
@param arrowSt arrow style.
@param arrowLe the arrow length.
@param arrowWi the arrow half width.
@param dashSt dash style.
@param font the name of the font for attached text.
@param size the size of the font for attached text.
*/
public PrimitiveComplexCurve(boolean f, boolean c, int layer,
boolean arrowS, boolean arrowE,
int arrowSt, int arrowLe, int arrowWi, int dashSt,
String font, int size)
{
super();
arrowData=new Arrow();
arrowData.setArrowStart(arrowS);
arrowData.setArrowEnd(arrowE);
arrowData.setArrowHalfWidth(arrowWi);
arrowData.setArrowLength(arrowLe);
arrowData.setArrowStyle(arrowSt);
dashStyle=dashSt;
p = null;
initPrimitive(storageSize, font, size);
nPoints=0;
isFilled=f;
isClosed=c;
dashStyle=dashSt;
setLayer(layer);
}
/** Add the given point to the closest part of the curve.
@param px the x coordinate of the point to add
@param py the y coordinate of the point to add
*/
public void addPointClosest(int px, int py)
{
int[] xp=new int[storageSize];
int[] yp=new int[storageSize];
int k;
for(k=0;k<nPoints;++k){
xp[k]=virtualPoint[k].x;
yp[k]=virtualPoint[k].y;
}
// Calculate the distance between the
// given point and all the segments composing the polygon and we
// take the smallest one.
int distance=(int)Math.sqrt((px-xp[0])*(px-xp[0])+
(py-yp[0])*(py-yp[0]));
int d;
int minv=0;
for(int i=0; i<q.getNpoints()-1; ++i) {
d=GeometricDistances.pointToSegment(q.getXpoints()[i],
q.getYpoints()[i], q.getXpoints()[i+1],
q.getYpoints()[i+1], px,py);
if(d<distance) {
distance = d;
minv=i-1;
}
}
minv /= STEPS;
++minv;
if(minv<0) minv=nPoints-1;
// Now minv contains the index of the vertex before the one which
// should be entered. We begin to enter the new vertex at the end...
addPoint(px, py);
// ...then we do the swap
int dummy;
for(int i=nPoints-1; i>minv; --i) {
virtualPoint[i].x=virtualPoint[i-1].x;
virtualPoint[i].y=virtualPoint[i-1].y;
}
virtualPoint[minv].x=px;
virtualPoint[minv].y=py;
changed = true;
}
/** Add a point at the current ComplexCurve. The point is always added
at the end of the already existing path.
@param x the x coordinate of the point.
@param y the y coordinate of the point.
*/
public void addPoint(int x, int y)
{
if(nPoints+2>=storageSize) {
int o_n=storageSize;
int i;
storageSize += 10;
PointG[] nv = new PointG[storageSize];
for(i=0;i<o_n;++i) {
nv[i]=virtualPoint[i];
}
for(;i<storageSize;++i) {
nv[i]=new PointG();
}
virtualPoint=nv;
}
// And now we enter the position of the point we are interested with
virtualPoint[nPoints].x=x;
virtualPoint[nPoints++].y=y;
// We do need to shift the two points describing the position
// of the text lines
virtualPoint[getNameVirtualPointNumber()].x=x+5;
virtualPoint[getNameVirtualPointNumber()].y=y+5;
virtualPoint[getValueVirtualPointNumber()].x=x+5;
virtualPoint[getValueVirtualPointNumber()].y=y+10;
changed = true;
}
/** Create the CurveStorage associated to the complex curve. This is a crude
technique, but it is very easy to implement.
@param coordSys the coordinate mapping to be employed.
@return the CurveStorage approximating the complex curve.
*/
public CurveStorage createComplexCurve(MapCoordinates coordSys)
{
int np=nPoints;
double [] xPoints = new double[np];
double [] yPoints = new double[np];
int i;
for (i=0; i<nPoints; ++i) {
xPoints[i] = coordSys.mapXr(virtualPoint[i].x,virtualPoint[i].y);
yPoints[i] = coordSys.mapYr(virtualPoint[i].x,virtualPoint[i].y);
}
// If the curve is closed, we need to add a last point which is the
// same as the first one.
Cubic[] X;
Cubic[] Y;
if(isClosed) {
X = calcNaturalCubicClosed(np-1, xPoints);
Y = calcNaturalCubicClosed(np-1, yPoints);
} else {
X = calcNaturalCubic(np-1, xPoints);
Y = calcNaturalCubic(np-1, yPoints);
}
if(X==null || Y==null) return null;
// very crude technique: just break each segment up into steps lines
CurveStorage c = new CurveStorage();
c.pp.add(new PointDouble(X[0].eval(0), Y[0].eval(0)));
int x, y;
for (i = 0; i < X.length; ++i) {
c.dd.add(new PointDouble(X[i].d1, Y[i].d1));
for (int j = 1; j <= STEPS; ++j) {
double u = j / (double) STEPS;
c.pp.add(new PointDouble(X[i].eval(u), Y[i].eval(u)));
}
}
c.dd.add(new PointDouble(X[X.length-1].d2, Y[X.length-1].d2));
return c;
}
/** Create the polygon associated to the complex curve. This is a crude
technique, but it is very easy to implement.
@param coordSys the coordinate mapping to be employed.
@param poly the polygon to which the points will be added.
@return the polygon approximating the complex curve.
*/
public PolygonInterface createComplexCurvePoly(MapCoordinates coordSys,
PolygonInterface poly)
{
xmin = Integer.MAX_VALUE;
ymin = Integer.MAX_VALUE;
int xmax = -Integer.MAX_VALUE;
int ymax = -Integer.MAX_VALUE;
CurveStorage c=createComplexCurve(coordSys);
if (c==null) return null;
Vector<PointDouble> pp = c.pp;
if (pp==null) return null;
int x, y;
for (int i = 0; i < pp.size(); ++i) {
x=(int)Math.round(pp.get(i).x);
y=(int)Math.round(pp.get(i).y);
poly.addPoint(x, y);
coordSys.trackPoint(x,y);
if (x<xmin)
xmin=x;
if (x>xmax)
xmax=x;
if(y<ymin)
ymin=y;
if(y>ymax)
ymax=y;
}
width = xmax-xmin;
height = ymax-ymin;
return poly;
}
/** Code adapted from Tim Lambert's snippets:
http://www.cse.unsw.edu.au/~lambert/splines/
Used here with permissions (hey, thanks a lot, Tim!).
@param n the number of points.
@param x the vector containing x coordinates of nodes.
@return the cubic curve spline'ing the nodes.
*/
Cubic[] calcNaturalCubic(int n, double... x)
{
if(n<1) return null;
double[] gamma = new double[n+1];
double[] delta = new double[n+1];
double[] D = new double[n+1];
int i;
/* We solve the equation
[2 1 ] [D[0]] [3(x[1] - x[0]) ]
|1 4 1 | |D[1]| |3(x[2] - x[0]) |
| 1 4 1 | | . | = | . |
| ..... | | . | | . |
| 1 4 1| | . | |3(x[n] - x[n-2])|
[ 1 2] [D[n]] [3(x[n] - x[n-1])]
by using row operations to convert the matrix to upper triangular
and then back substitution. The D[i] are the derivatives at the knots.
*/
gamma[0] = 1.0/2.0;
for (i = 1; i<n; ++i) {
gamma[i] = 1.0/(4.0-gamma[i-1]);
}
gamma[n] = 1.0/(2.0-gamma[n-1]);
delta[0] = 3*(x[1]-x[0])*gamma[0];
for (i = 1; i < n; ++i) {
delta[i] = (3.0*(x[i+1]-x[i-1])-delta[i-1])*gamma[i];
}
delta[n] = (3.0*(x[n]-x[n-1])-delta[n-1])*gamma[n];
D[n] = delta[n];
for (i = n-1; i>=0; --i) {
D[i] = delta[i] - gamma[i]*D[i+1];
}
/* now compute the coefficients of the cubics */
Cubic[] C = new Cubic[n];
for (i = 0; i<n; ++i) {
C[i] = new Cubic(x[i], D[i], 3.0*(x[i+1] - x[i]) -2.0*D[i]-D[i+1],
2.0*(x[i] - x[i+1]) + D[i] + D[i+1]);
C[i].d1=D[i];
C[i].d2=D[i+1];
}
return C;
}
/** Code mainly taken from Tim Lambert's snippets:
http://www.cse.unsw.edu.au/~lambert/splines/
Used here with permissions (hey, thanks a lot, Tim!)
calculates the closed natural cubic spline that interpolates
x[0], x[1], ... x[n]
The first segment is returned as
C[0].a + C[0].b*u + C[0].c*u^2 + C[0].d*u^3 0<=u <1
the other segments are in C[1], C[2], ... C[n] */
Cubic[] calcNaturalCubicClosed(int n, double... x)
{
if(n<1) return null;
double[] w = new double[n+1];
double[] v = new double[n+1];
double[] y = new double[n+1];
double[] D = new double[n+1];
double z, F, G, H;
int k;
/* We solve the equation
[4 1 1] [D[0]] [3(x[1] - x[n]) ]
|1 4 1 | |D[1]| |3(x[2] - x[0]) |
| 1 4 1 | | . | = | . |
| ..... | | . | | . |
| 1 4 1| | . | |3(x[n] - x[n-2])|
[1 1 4] [D[n]] [3(x[0] - x[n-1])]
by decomposing the matrix into upper triangular and lower matrices
and then back substitution. See Spath "Spline Algorithms for
Curves and Surfaces" pp 19--21. The D[i] are the derivatives at
the knots.
*/
w[1] = v[1] = z = 1.0f/4.0f;
y[0] = z * 3 * (x[1] - x[n]);
H = 4;
F = 3 * (x[0] - x[n-1]);
G = 1;
for (k = 1; k < n; ++k) {
v[k+1] = z = 1/(4 - v[k]);
w[k+1] = -z * w[k];
y[k] = z * (3*(x[k+1]-x[k-1]) - y[k-1]);
H = H - G * w[k];
F = F - G * y[k-1];
G = -v[k] * G;
}
H = H - (G+1)*(v[n]+w[n]);
y[n] = F - (G+1)*y[n-1];
D[n] = y[n]/H;
D[n-1] = y[n-1] - (v[n]+w[n])*D[n]; /* This equation is WRONG! in
my copy of Spath */
for (k = n-2; k >= 0; --k) {
D[k] = y[k] - v[k+1]*D[k+1] - w[k+1]*D[n];
}
/* now compute the coefficients of the cubics */
Cubic[] C = new Cubic[n+1];
for (k = 0; k < n; ++k) {
C[k] = new Cubic((float)x[k], D[k],
3*(x[k+1] - x[k]) - 2*D[k] - D[k+1],
2*(x[k] - x[k+1]) + D[k] + D[k+1]);
C[k].d1=D[k];
C[k].d2=D[k+1];
}
C[n] = new Cubic((float)x[n], D[n], 3*(x[0] - x[n]) - 2*D[n] - D[0],
2*(x[n] - x[0]) + D[n] + D[0]);
C[n].d1=D[n];
C[n].d2=D[0];
return C;
}
/** Remove the control point of the spline closest to the given
coordinates, if the distance is less than a certain tolerance
@param x the x coordinate of the target
@param y the y coordinate of the target
@param tolerance the tolerance
*/
public void removePoint(int x, int y, double tolerance)
{
// We can not have a spline with less than three vertices
if (nPoints<=3)
return;
int i;
double distance;
double min_distance= GeometricDistances.pointToPoint(virtualPoint[0].x,
virtualPoint[0].y,x,y);
int sel_i=-1;
for(i=1;i<nPoints;++i) {
distance = GeometricDistances.pointToPoint(virtualPoint[i].x,
virtualPoint[i].y,x,y);
if (distance<min_distance) {
min_distance=distance;
sel_i=i;
}
}
// Check if the control node losest to the given coordinates
// is closer than the given tolerance
if(min_distance<=tolerance){
--nPoints;
for(i=0;i<nPoints;++i) {
// Shift all the points subsequent to the one which needs
// to be erased.
if(i>=sel_i) {
virtualPoint[i].x=virtualPoint[i+1].x;
virtualPoint[i].y=virtualPoint[i+1].y;
}
changed=true;
}
}
}
/** Draw the graphic primitive on the given graphic context.
@param g the graphic context in which the primitive should be drawn.
@param coordSys the graphic coordinates system to be applied.
@param layerV the layer description.
*/
public void draw(GraphicsInterface g, MapCoordinates coordSys,
Vector layerV)
{
if(!selectLayer(g,layerV))
return;
drawText(g, coordSys, layerV, -1);
if(changed) {
changed=false;
// Important: note that createComplexCurve has some important
// side effects as the update of the xmin, ymin, width and height
// variables. This means that the order of the two following
// commands is important!
q=createComplexCurvePoly(new MapCoordinates(), g.createPolygon());
p=createComplexCurvePoly(coordSys, g.createPolygon());
CurveStorage c = createComplexCurve(coordSys);
// Prevent a null pointer exception when the user does three clicks
// on the same point. TODO: an incomplete toString output is
// created.
if (c==null)
return;
Vector<PointDouble> dd = c.dd;
Vector<PointDouble> pp = c.pp;
if(q==null) return;
gp = g.createShape();
gp.createGeneralPath(q.getNpoints());
gp.moveTo((float)pp.get(0).x, (float)pp.get(0).y);
int increment=STEPS;
double derX1=0.0, derX2=0.0;
double derY1=0.0, derY2=0.0;
double w1=0.666667, w2=0.666667;
int j=0;
for(int i=0; i<pp.size()-increment; i+=increment) {
derX1=dd.get(j).x/2.0*w1;
derY1=dd.get(j).y/2.0*w1;
derX2=dd.get(j+1).x/2.0*w2;
derY2=dd.get(j+1).y/2.0*w2;
++j;
gp.curveTo((float)(pp.get(i).x+derX1),
(float)(pp.get(i).y+derY1),
(float)(pp.get(i+increment).x-derX2),
(float)(pp.get(i+increment).y-derY2),
(float)(pp.get(i+increment).x),
(float)(pp.get(i+increment).y));
}
if (isClosed) gp.closePath();
w = (float)(Globals.lineWidth*coordSys.getXMagnitude());
if (w<D_MIN) w=D_MIN;
}
if (p==null || gp==null)
return;
// If the curve is outside of the shown portion of the drawing,
// exit immediately.
if(!g.hitClip(xmin,ymin, width+1, height+1))
return;
g.applyStroke(w, dashStyle);
// If needed, fill the interior of the shape
if (isFilled) {
g.fill(gp);
}
if(width==0 || height==0) {
// Degenerate case: draw a segment.
int d=nPoints-1;
g.drawLine(coordSys.mapX(virtualPoint[0].x,virtualPoint[0].y),
coordSys.mapY(virtualPoint[0].x,virtualPoint[0].y),
coordSys.mapX(virtualPoint[d].x,virtualPoint[d].y),
coordSys.mapY(virtualPoint[d].x,virtualPoint[d].y));
} else {
// Draw the curve.
g.draw(gp);
}
// Ensure that there are enough points to calculate the derivative.
if (p.getNpoints()<2)
return;
// Draw the arrows if they are needed
if (arrowData.atLeastOneArrow()) {
arrowData.prepareCoordinateMapping(coordSys);
if (arrowData.isArrowStart()&&!isClosed) {
arrowData.drawArrow(g, p.getXpoints()[0], p.getYpoints()[0],
p.getXpoints()[1], p.getYpoints()[1]);
}
if (arrowData.isArrowEnd()&&!isClosed) {
arrowData.drawArrow(g, p.getXpoints()[p.getNpoints()-1],
p.getYpoints()[p.getNpoints()-1],
p.getXpoints()[p.getNpoints()-2],
p.getYpoints()[p.getNpoints()-2]);
}
}
}
/** Parse a token array and store the graphic data for a given primitive
Obviously, that routine should be called *after* having recognized
that the called primitive is correct.
That routine also sets the current layer.
@param tokens the tokens to be processed. tokens[0] should be the
command of the actual primitive.
@param N the number of tokens present in the array.
@throws IOException it parsing goes wrong, parameters can not be read
or primitive is incorrect.
*/
public void parseTokens(String[] tokens, int N)
throws IOException
{
changed=true;
// assert it is the correct primitive
if (tokens[0].equals("CP")||tokens[0].equals("CV")) {
if (N<6) {
IOException E=new IOException("bad arguments on CP/CV");
throw E;
}
// Load the points in the virtual points associated to the
// current primitive.
int j=1;
int i=0;
int x1 = 0;
int y1 = 0;
// The first token says if the spline is opened or closed
if(tokens[j++].equals("1"))
isClosed = true;
else
isClosed = false;
// Then we have the points defining the curve
while(j<N-1) {
if (j+1<N-1 && tokens[j+1].equals("FCJ"))
break;
x1 =Integer.parseInt(tokens[j++]);
// Check if the following point is available
if(j>=N-1) {
IOException E=new IOException("bad arguments on CP/CV");
throw E;
}
y1 =Integer.parseInt(tokens[j++]);
++i;
addPoint(x1,y1);
}
nPoints=i;
// We specify now the standard position of the name and value
virtualPoint[getNameVirtualPointNumber()].x=x1+5;
virtualPoint[getNameVirtualPointNumber()].y=y1+5;
virtualPoint[getValueVirtualPointNumber()].x=x1+5;
virtualPoint[getValueVirtualPointNumber()].y=y1+10;
// And we check finally for extensions (FCJ)
if(N>j) {
parseLayer(tokens[j++]);
if(N>j && tokens[j++].equals("FCJ")) {
j=arrowData.parseTokens(tokens, j);
dashStyle = checkDashStyle(Integer.parseInt(tokens[j++]));
}
}
// See if the curve should be filled (command CP) or empty (CV)
if (tokens[0].equals("CP"))
isFilled=true;
else
isFilled=false;
} else {
IOException E=new IOException("CP/CV: Invalid primitive:"+tokens[0]+
" programming error?");
throw E;
}
}
/** Get the control parameters of the given primitive.
@return a vector of ParameterDescription containing each control
parameter.
The first parameters should always be the virtual points.
*/
public Vector<ParameterDescription> getControls()
{
Vector<ParameterDescription> v=super.getControls();
ParameterDescription pd = new ParameterDescription();
pd.parameter=Boolean.valueOf(isFilled);
pd.description=Globals.messages.getString("ctrl_filled");
v.add(pd);
pd = new ParameterDescription();
pd.parameter=Boolean.valueOf(isClosed);
pd.description=Globals.messages.getString("ctrl_closed_curve");
pd.isExtension = true;
v.add(pd);
arrowData.getControlsForArrow(v);
pd = new ParameterDescription();
pd.parameter=new DashInfo(dashStyle);
pd.description=Globals.messages.getString("ctrl_dash_style");
pd.isExtension = true;
v.add(pd);
return v;
}
/** Set the control parameters of the given primitive.
This method is specular to getControls().
@param v a vector of ParameterDescription containing each control
parameter. The first parameters should always be the virtual
points.
@return the next index in v to be scanned (if needed) after the
execution of this function.
*/
public int setControls(Vector<ParameterDescription> v)
{
int i=super.setControls(v);
ParameterDescription pd;
pd=(ParameterDescription)v.get(i);
++i;
// Check, just for sure...
if (pd.parameter instanceof Boolean)
isFilled=((Boolean)pd.parameter).booleanValue();
else
System.out.println("Warning: unexpected parameter!"+pd);
pd=(ParameterDescription)v.get(i++);
// Check, just for sure...
if (pd.parameter instanceof Boolean)
isClosed=((Boolean)pd.parameter).booleanValue();
else
System.out.println("Warning: unexpected parameter!"+pd);
i=arrowData.setParametersForArrow(v, i);
pd=(ParameterDescription)v.get(i++);
if (pd.parameter instanceof DashInfo)
dashStyle=((DashInfo)pd.parameter).style;
else
System.out.println("Warning: unexpected parameter 6!"+pd);
// Parameters validation and correction
if(dashStyle>=Globals.dashNumber)
dashStyle=Globals.dashNumber-1;
if(dashStyle<0)
dashStyle=0;
return i;
}
/** Gets the distance (in primitive's coordinates space) between a
given point and the primitive.
When it is reasonable, the behaviour can be binary (ComplexCurves,
ovals...). In other cases (lines, points), it can be proportional.
@param px the x coordinate of the given point.
@param py the y coordinate of the given point.
@return the distance in logical units.
*/
public int getDistanceToPoint(int px, int py)
{
// Here we check if the given point lies inside the text areas
if(checkText(px, py))
return 0;
int distance = 100;
// In this case, the user has not introduced a complete curve,
// but just one point.
if(p==null) {
return GeometricDistances.pointToPoint(virtualPoint[0].x,
virtualPoint[0].y,
px,py);
}
// If the curve is filled, we check if the given point lies inside
// the polygon.
if(isFilled && q.contains(px, py)) {
return 1;
}
// If the curve is not filled, we calculate the distance between the
// given point and all the segments composing the curve and we
// take the smallest one.
int [] xpoints=q.getXpoints();
int [] ypoints=q.getYpoints();
for(int i=0; i<q.getNpoints()-1; ++i) {
int d=GeometricDistances.pointToSegment(xpoints[i], ypoints[i],
xpoints[i+1], ypoints[i+1], px,py);
if(d<distance)
distance = d;
}
return distance;
}
/** Obtain a string command descripion of the primitive.
@param extensions true if FidoCadJ extensions to the old FidoCAD format
should be active.
@return the FidoCadJ command line.
*/
public String toString(boolean extensions)
{
// A single point curve without anything is not worth converting.
if (name.length()==0 && value.length()==0 && nPoints==1) {
return "";
}
StringBuffer temp=new StringBuffer(25);
if(isFilled)
temp.append("CP ");
else
temp.append("CV ");
if(isClosed)
temp.append("1 ");
else
temp.append("0 ");
for(int i=0; i<nPoints;++i) {
temp.append(virtualPoint[i].x);
temp.append(" ");
temp.append(virtualPoint[i].y);
temp.append(" ");
}
temp.append(getLayer());
temp.append("\n");
String cmd=temp.toString();
if(extensions && (arrowData.atLeastOneArrow() || dashStyle>0 ||
hasName() || hasValue()))
{
String text = "0";
// We take into account that there may be some text associated
// to that primitive.
if (name.length()!=0 || value.length()!=0)
text = "1";
cmd+="FCJ "+arrowData.createArrowTokens()+" "+text+"\n";
}
// The false is needed since saveText should not write the FCJ tag.
cmd+=saveText(false);
return cmd;
}
/** Export the primitive on a vector graphic format.
@param exp the export interface to employ.
@param cs the coordinate mapping to employ.
@throws IOException if a problem occurs, such as it is impossible to
write on the output file.
*/
public void export(ExportInterface exp, MapCoordinates cs)
throws IOException
{
double [] xPoints = new double[nPoints];
double [] yPoints = new double[nPoints];
PointDouble[] vertices = new PointDouble[nPoints*STEPS+1];
int i;
for (i=0; i<nPoints; ++i) {
xPoints[i] = cs.mapXr(virtualPoint[i].x,virtualPoint[i].y);
yPoints[i] = cs.mapYr(virtualPoint[i].x,virtualPoint[i].y);
// This is a trick: we do not use another array, but we pre-charge
// the control points in vertices (sure we have some place, at
// least if STEPS>-1). If the export is done via a polygon, those
// points will be discarded and the array reused.
vertices[i] = new PointDouble();
vertices[i].x = xPoints[i];
vertices[i].y = yPoints[i];
}
// Check if the export is handled via a dedicated curve primitive.
// If not, we continue using a polygon with an high number of
// vertex
if (!exp.exportCurve(vertices, nPoints, isFilled, isClosed, getLayer(),
arrowData.isArrowStart(), arrowData.isArrowEnd(),
arrowData.getArrowStyle(),
(int)(arrowData.getArrowLength()*cs.getXMagnitude()),
(int)(arrowData.getArrowHalfWidth()*cs.getXMagnitude()),
dashStyle, Globals.lineWidth*cs.getXMagnitude()))
{
exportAsPolygonInterface(xPoints, yPoints, vertices, exp, cs);
int totalnP=q.getNpoints();
//System.out.println("totalnP="+totalnP);
// Draw the arrows if they are needed
if(q.getNpoints()>2) {
if (arrowData.isArrowStart()&&!isClosed) {
exp.exportArrow(vertices[0].x, vertices[0].y,
vertices[1].x, vertices[1].y,
arrowData.getArrowLength()*cs.getXMagnitude(),
arrowData.getArrowHalfWidth()*cs.getXMagnitude(),
arrowData.getArrowStyle());
}
if (arrowData.isArrowEnd()&&!isClosed) {
exp.exportArrow(vertices[totalnP-1].x,
vertices[totalnP-1].y,
vertices[totalnP-2].x, vertices[totalnP-2].y,
arrowData.getArrowLength()*cs.getXMagnitude(),
arrowData.getArrowHalfWidth()*cs.getXMagnitude(),
arrowData.getArrowStyle());
}
}
}
exportText(exp, cs, -1);
}
/** Expansion of the curve in a polygon with a big number of corners.
This is useful when some sort of spline command is not available on
the export format chosen.
*/
private void exportAsPolygonInterface(double [] xPoints, double [] yPoints,
PointDouble[] vertices,
ExportInterface exp, MapCoordinates cs)
throws IOException
{
Cubic[] X;
Cubic[] Y;
int i;
if(isClosed) {
X = calcNaturalCubicClosed(nPoints-1, xPoints);
Y = calcNaturalCubicClosed(nPoints-1, yPoints);
} else {
X = calcNaturalCubic(nPoints-1, xPoints);
Y = calcNaturalCubic(nPoints-1, yPoints);
}
if(X==null || Y==null) return;
/* very crude technique - just break each segment up into steps lines */
vertices[0]=new PointDouble();
vertices[0].x=X[0].eval(0);
vertices[0].y=Y[0].eval(0);
int x, y;
for (i = 0; i < X.length; ++i) {
for (int j = 1; j <= STEPS; ++j) {
double u = j / (double) STEPS;
vertices[i*STEPS+j]=new PointDouble();
vertices[i*STEPS+j].x=X[i].eval(u);
vertices[i*STEPS+j].y=Y[i].eval(u);
}
}
vertices[X.length*STEPS]=new PointDouble();
vertices[X.length*STEPS].x=X[X.length-1].eval(1.0);
vertices[X.length*STEPS].y=Y[X.length-1].eval(1.0);
if (isClosed) {
exp.exportPolygon(vertices, X.length*STEPS+1, isFilled,
getLayer(),
dashStyle, Globals.lineWidth*cs.getXMagnitude());
} else {
for(i=1; i<X.length*STEPS+1;++i){
exp.exportLine(vertices[i-1].x,
vertices[i-1].y,
vertices[i].x,
vertices[i].y,
getLayer(),
false, false,
0, 0, 0,
dashStyle,
Globals.lineWidth*cs.getXMagnitude());
}
}
}
/** Get the number of the virtual point associated to the Name property
@return the number of the virtual point associated to the Name property
*/
public int getNameVirtualPointNumber()
{
return nPoints;
}
/** Get the number of the virtual point associated to the Value property
@return the number of the virtual point associated to the Value
property
*/
public int getValueVirtualPointNumber()
{
return nPoints+1;
}
}
/** this class represents a cubic polynomial, by Tim Lambert */
class Cubic
{
double a,b,c,d; /* a + b*u + c*u^2 +d*u^3 */
public double d1, d2; // Derivatives
public Cubic(double a, double b, double c, double d)
{
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
/** evaluate cubic */
public double eval(double u)
{
return ((d*u + c)*u + b)*u + a;
}
}
/** The curve is stored in two vectors.
The first contains a curve representation as a polygon with a lot of
vertices.
The second has as much as elements as the number of control vertices and
stores only the derivatives.
*/
class CurveStorage
{
Vector<PointDouble> pp; // Curve as a polygon (relatively big)
Vector<PointDouble> dd; // Derivatives
public CurveStorage()
{
pp = new Vector<PointDouble>();
dd = new Vector<PointDouble>();
}
}