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>(); } }