/* GeoGebra - Dynamic Mathematics for Everyone http://www.geogebra.org This file is part of GeoGebra. This program 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. */ package org.geogebra.common.kernel.geos; import java.util.ArrayList; import org.apache.commons.math3.analysis.UnivariateFunction; import org.geogebra.common.kernel.Construction; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.MatrixTransformable; import org.geogebra.common.kernel.ParametricCurveDistanceFunction; import org.geogebra.common.kernel.PathMover; import org.geogebra.common.kernel.PathMoverGeneric; import org.geogebra.common.kernel.PathParameter; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.Matrix.Coords; import org.geogebra.common.kernel.algos.AlgoMacroInterface; import org.geogebra.common.kernel.arithmetic.ExpressionNode; import org.geogebra.common.kernel.arithmetic.Function; import org.geogebra.common.kernel.arithmetic.MyDouble; import org.geogebra.common.kernel.arithmetic.NumberValue; import org.geogebra.common.kernel.arithmetic.ValueType; import org.geogebra.common.kernel.kernelND.GeoConicNDConstants; import org.geogebra.common.kernel.kernelND.GeoCurveCartesianND; import org.geogebra.common.kernel.kernelND.GeoElementND; import org.geogebra.common.kernel.kernelND.GeoLineND; import org.geogebra.common.kernel.kernelND.GeoPointND; import org.geogebra.common.kernel.optimization.ExtremumFinderI; import org.geogebra.common.kernel.roots.RealRootUtil; import org.geogebra.common.plugin.GeoClass; import org.geogebra.common.plugin.Operation; import org.geogebra.common.util.MyMath; /** * Cartesian parametric curve, e.g. (cos(t), sin(t)) for t from 0 to 2pi. * * @author Markus Hohenwarter */ public class GeoCurveCartesian extends GeoCurveCartesianND implements Transformable, Translateable, PointRotateable, Mirrorable, Dilateable, MatrixTransformable, ConicMirrorable { // private Function funX, funY; private boolean isClosedPath; private boolean trace = false; /** * Creates new curve * * @param c * construction * */ public GeoCurveCartesian(Construction c) { super(c, 2, null); } /** * Creates new curve * * @param c * construction * @param fx * x-coord function * @param fy * y-coord function * @param point * point expression */ public GeoCurveCartesian(Construction c, Function fx, Function fy, ExpressionNode point) { super(c, 2, point); setFunctionX(fx); setFunctionY(fy); } @Override public String translatedTypeString() { return this.getLoc().getPlain("Curve"); } @Override public GeoClass getGeoClassType() { return GeoClass.CURVE_CARTESIAN; } /** * copy constructor * * @param f * Curve to copy */ public GeoCurveCartesian(GeoCurveCartesian f) { super(f.cons, 2, null); set(f); } @Override public GeoElement copy() { return new GeoCurveCartesian(this); } /** * Sets the function of the x coordinate of this curve. * * @param funX * new x-coord function */ final public void setFunctionX(Function funX) { setFun(0, funX); } /** * Sets the function of the y coordinate of this curve. * * @param funY * new y-coord function */ final public void setFunctionY(Function funY) { setFun(1, funY); } /** * Replaces geo and all its dependent geos in this function's expression by * copies of their values. * * @param geo * Element to be replaced */ @Override public void replaceChildrenByValues(GeoElement geo) { if (getFun(0) != null) { getFun(0).replaceChildrenByValues(geo); } if (getFun(1) != null) { getFun(1).replaceChildrenByValues(geo); } } /** * Sets the start and end parameter value of this curve. Note: * setFunctionX() and setFunctionY() has to be called before this method. */ @Override public void setInterval(double startParam, double endParam) { /* * this.startParam = startParam; this.endParam = endParam; * * isDefined = startParam <= endParam; */ super.setInterval(startParam, endParam); // update isClosedPath, i.e. startPoint == endPoint this.isClosedPath = Kernel.isEqual(getFun(0).value(startParam), getFun(0).value(endParam), Kernel.MIN_PRECISION) && Kernel.isEqual(getFun(1).value(startParam), getFun(1).value(endParam), Kernel.MIN_PRECISION); } @Override public void set(GeoElementND geo) { GeoCurveCartesian geoCurve = (GeoCurveCartesian) geo; setFun(0, new Function(geoCurve.fun[0], this.kernel)); setFun(1, new Function(geoCurve.fun[1], this.kernel)); this.startParam = geoCurve.startParam; this.endParam = geoCurve.endParam; this.isDefined = geoCurve.isDefined; // macro OUTPUT if (geo.getConstruction() != this.cons && isAlgoMacroOutput()) { if (!geo.isIndependent()) { // System.out.println("set " + this.label); // System.out.println(" funX before: " + // funX.toLaTeXString(true)); // this object is an output object of AlgoMacro // we need to check the references to all geos in its function's // expression AlgoMacroInterface algoMacro = (AlgoMacroInterface) getParentAlgorithm(); algoMacro.initFunction(getFun(0)); algoMacro.initFunction(getFun(1)); // System.out.println(" funX after: " + // funX.toLaTeXString(true)); } } this.distFun = null; } /** * Sets this curve to the parametric derivative of the given curve c. The * parametric derivative of a curve c(t) = (x(t), y(t)) is defined as (x(t), * y'(t)/x'(t)). * * @param curve * curve whose derivative we want */ public void setParametricDerivative(GeoCurveCartesian curve) { if (curve.isDefined()) { setFun(0, curve.fun[0]); setFun(1, Function.getDerivativeQuotient(curve.fun[0], curve.fun[1])); this.isDefined = !(getFun(0) == null || getFun(1) == null); if (this.isDefined) { setInterval(curve.startParam, curve.endParam); } } else { this.isDefined = false; } this.distFun = null; } // added by Loic Le Coq 2009/08/12 /** * @param tpl * string template * @return value string x-coord function */ final public String getFunX(StringTemplate tpl) { return getFun(0).toValueString(tpl); } /** * @param tpl * string template * @return value string y-coord function */ final public String getFunY(StringTemplate tpl) { return getFun(1).toValueString(tpl); } // end Loic Le Coq @Override final public UnivariateFunction getUnivariateFunctionX() { return getFun(0); } @Override final public UnivariateFunction getUnivariateFunctionY() { return getFun(1); } /** * translate function by vector v */ @Override final public void translate(Coords v) { getFun(0).translateY(v.getX()); getFun(1).translateY(v.getY()); } @Override final public boolean isTranslateable() { return true; } @Override final public boolean isMatrixTransformable() { return true; } /** * Translates the curve by vector given by coordinates * * @param vx * x-coord of the translation vector * @param vy * y-coord of the translation vector */ final public void translate(double vx, double vy) { getFun(0).translateY(vx); getFun(1).translateY(vy); } @Override final public void rotate(NumberValue phi, GeoPointND point) { Coords P = point.getInhomCoords(); translate(-P.getX(), -P.getY()); rotate(phi); translate(P.getX(), P.getY()); } @Override final public void mirror(Coords P) { dilate(new MyDouble(this.kernel, -1.0), P); } @Override final public void mirror(GeoLineND g1) { GeoLine g = (GeoLine) g1; // Y = S(phi).(X - Q) + Q // where Q is a point on g, S(phi) is the mirrorTransform(phi) // and phi/2 is the line's slope angle // get arbitrary point of line double qx, qy; if (Math.abs(g.getX()) > Math.abs(g.getY())) { qx = g.getZ() / g.getX(); qy = 0.0d; } else { qx = 0.0d; qy = g.getZ() / g.getY(); } // translate -Q translate(qx, qy); // S(phi) mirror(new MyDouble(this.kernel, 2.0 * Math.atan2(-g.getX(), g.getY()))); // translate back +Q translate(-qx, -qy); // update inhom coords } @Override final public void rotate(NumberValue phi) { double cosPhi = Math.cos(phi.getDouble()); double sinPhi = Math.sin(phi.getDouble()); matrixTransform(cosPhi, -sinPhi, sinPhi, cosPhi); } @Override public void dilate(NumberValue ratio, Coords P) { translate(-P.getX(), -P.getY()); ExpressionNode exprX = getFun(0).deepCopy(this.kernel).getExpression(); ExpressionNode exprY = getFun(1).deepCopy(this.kernel).getExpression(); getFun(0).setExpression(new ExpressionNode(this.kernel, ratio, Operation.MULTIPLY, exprX)); getFun(1).setExpression(new ExpressionNode(this.kernel, ratio, Operation.MULTIPLY, exprY)); translate(P.getX(), P.getY()); } /** * mirror transform with angle phi [ cos(phi) sin(phi) ] [ sin(phi) * -cos(phi) ] */ private void mirror(NumberValue phi) { double cosPhi = Math.cos(phi.getDouble()); double sinPhi = Math.sin(phi.getDouble()); matrixTransform(cosPhi, sinPhi, sinPhi, -cosPhi); } /** * return n different points on curve, needs for inversion * * @param n * number of requested points * @param startInterval * least value of param * @param endInterval * highest value of param * @return array list of points */ public ArrayList<GeoPoint> getPointsOnCurve(int n, double startInterval, double endInterval) { ArrayList<GeoPoint> pointList = new ArrayList<GeoPoint>(); double step = (endInterval - startInterval) / (n + 1); for (double i = 0, v = startInterval; i < n; i++, v += step) { double[] point = new double[2]; point[0] = getFun(0).value(v); point[1] = getFun(1).value(v); pointList.add(new GeoPoint(this.cons, point[0], point[1], 1)); } return pointList; } /** * Transforms curve using matrix [a b] [c d] * * @param a * top left matrix element * @param b * top right matrix element * @param c * bottom left matrix element * @param d * bottom right matrix element */ @Override public void matrixTransform(double a, double b, double c, double d) { MyDouble ma = new MyDouble(this.kernel, a); MyDouble mb = new MyDouble(this.kernel, b); MyDouble mc = new MyDouble(this.kernel, c); MyDouble md = new MyDouble(this.kernel, d); ExpressionNode exprX = getFun(0).deepCopy(this.kernel).getExpression(); ExpressionNode exprY = getFun(1).deepCopy(this.kernel).getExpression(); ExpressionNode transX = exprX.multiply(ma).plus(exprY.multiply(mb)); ExpressionNode transY = exprX.multiply(mc).plus(exprY.multiply(md)); getFun(0).setExpression(transX); getFun(1).setExpression(transY); } @Override public boolean showInAlgebraView() { return true; } @Override protected boolean showInEuclidianView() { return isDefined(); } // TODO remove and use super method (funX and funY should be removed in // fun[]) @Override public String toString(StringTemplate tpl) { StringBuilder sbToString = new StringBuilder(80); sbToString.setLength(0); if (isLabelSet()) { sbToString.append(this.label); // sbToString.append('('); // sbToString.append(funX.getVarString()); // sbToString.append(") = "); // changed to ':' to make LaTeX output better sbToString.append(':'); } sbToString.append(toValueString(tpl)); return sbToString.toString(); } // private StringBuilder sbToString; // private StringBuilder sbTemp; // TODO remove and use super method (funX and funY should be removed in // fun[]) @Override public String toValueString(StringTemplate tpl) { Function funx = getFun(0); Function funy = getFun(1); if (this.isDefined && funx != null && funy != null) { StringBuilder sbTemp = new StringBuilder(80); if (tpl.hasCASType()) { // eg plotparam([t,t^2],t,-10,10) // TODO: remove wrapping in equation when Giac supports // intersecting equation, parametric sbTemp.append("equation(plotparam(["); sbTemp.append(funx.toValueString(tpl)); sbTemp.append(','); sbTemp.append(funy.toValueString(tpl)); sbTemp.append("],"); sbTemp.append(funx.getFunctionVariable() .toString(StringTemplate.giacTemplate)); sbTemp.append(','); sbTemp.append(this.kernel.format(getMinParameter(), StringTemplate.giacTemplate)); sbTemp.append(','); sbTemp.append(this.kernel.format(getMaxParameter(), StringTemplate.giacTemplate)); sbTemp.append("))"); } else { sbTemp.append('('); sbTemp.append(funx.toValueString(tpl)); sbTemp.append(", "); sbTemp.append(funy.toValueString(tpl)); sbTemp.append(')'); } return sbTemp.toString(); } return "?"; } // TODO remove and use super method (funX and funY should be removed in // fun[]) @Override public String toSymbolicString(StringTemplate tpl) { if (this.isDefined) { StringBuilder sbTemp = new StringBuilder(80); sbTemp.setLength(0); sbTemp.append('('); sbTemp.append(getFun(0).toString(tpl)); sbTemp.append(", "); sbTemp.append(getFun(1).toString(tpl)); sbTemp.append(')'); return sbTemp.toString(); } return "?"; } /* * Path interface */ @Override public void pointChanged(GeoPointND P) { // get closest parameter position on curve PathParameter pp = P.getPathParameter(); double t = getClosestParameter(P, pp.t); pp.t = t; pathChanged(P, false); } @Override public boolean isOnPath(GeoPointND PI, double eps) { GeoPoint P = (GeoPoint) PI; if (P.getPath() == this) { return true; } // get closest parameter position on curve PathParameter pp = P.getPathParameter(); double t = getClosestParameter(P, pp.t); boolean onPath = Math.abs(getFun(0).value(t) - P.getInhomX()) <= eps && Math.abs(getFun(1).value(t) - P.getInhomY()) <= eps; return onPath; } @Override public void pathChanged(GeoPointND PI) { // if kernel doesn't use path/region parameters, do as if point changed // its coords pathChanged(PI, !getKernel().usePathAndRegionParameters(PI)); } private void pathChanged(GeoPointND P, boolean changePoint) { // if kernel doesn't use path/region parameters, do as if point changed // its coords if (changePoint) { pointChanged(P); return; } PathParameter pp = P.getPathParameter(); if (pp.t < this.startParam) { pp.t = this.startParam; } else if (pp.t > this.endParam) { pp.t = this.endParam; } // calc point for given parameter P.setCoords2D(getFun(0).value(pp.t), getFun(1).value(pp.t), 1); P.updateCoordsFrom2D(false, null); } @Override public void updateDistanceFunction() { if (this.distFun == null) { this.distFun = new ParametricCurveDistanceFunction(this); } ((ParametricCurveDistanceFunction) distFun).setFunctions(this); } /** * Returns the parameter value t where this curve has minimal distance to * point P. * * @param startValue * an interval around startValue is specially investigated * @param P * point to which the distance is minimized * @return optimal parameter value t */ @Override public double getClosestParameter(GeoPointND P, double startValue) { double startVal = startValue; if (this.distFun == null) { this.distFun = new ParametricCurveDistanceFunction(this); } this.distFun.setDistantPoint(P); // check if P is on this curve and has the right path parameter already if (P.getPath() == this) { // point A is on curve c, take its parameter PathParameter pp = P.getPathParameter(); double pathParam = pp.t; if (this.distFun.value(pathParam) < Kernel.MIN_PRECISION * Kernel.MIN_PRECISION) { return pathParam; } // if we don't have a startValue yet, let's take the path parameter // as a guess if (Double.isNaN(startVal)) { startVal = pathParam; } } // first sample distFun to find a start interval for ExtremumFinder double step = (this.endParam - this.startParam) / CLOSEST_PARAMETER_SAMPLES; double minVal = this.distFun.value(this.startParam); double minParam = this.startParam; double t = this.startParam; for (int i = 0; i < CLOSEST_PARAMETER_SAMPLES; i++) { t = t + step; double ft = this.distFun.value(t); if (ft < minVal) { // found new minimum minVal = ft; minParam = t; } } // use interval around our minParam found by sampling // to find minimum // Math.max/min removed and ParametricCurveDistanceFunction modified // instead // TRAC-4583 #4567 removed wrong check, put Math.max/min in double left = Math.max(this.getMinParameter(), minParam - step); double right = Math.min(this.getMaxParameter(), minParam + step); ExtremumFinderI extFinder = this.kernel.getExtremumFinder(); double sampleResult = extFinder.findMinimum(left, right, this.distFun, Kernel.MIN_PRECISION); sampleResult = adjustRange(sampleResult); // if we have a valid startParam we try the interval around it too // however, we don't check the same interval again if (!Double.isNaN(startVal) && (startVal < left || right < startVal)) { // Math.max/min removed and ParametricCurveDistanceFunction modified // instead left = startVal - step; right = startVal + step; double startValResult = extFinder.findMinimum(left, right, this.distFun, Kernel.MIN_PRECISION); startValResult = adjustRange(startValResult); if (this.distFun .value(startValResult) < this.distFun.value(sampleResult) + Kernel.MIN_PRECISION / 2) { return startValResult; } } return sampleResult; } /** * allow a curve like Curve[sin(t), cos(t), t, 0, 12*2pi] to "join up" * properly at 0 and 12*2pi * * @param startValResult * @return startValResult adjusted to be in range [startParam, endParam] if * it's just outside */ private double adjustRange(double startValResult) { if (startValResult < this.startParam) { return startValResult + (this.endParam - this.startParam); } if (startValResult > this.endParam) { return startValResult - (this.endParam - this.startParam); } return startValResult; } @Override public PathMover createPathMover() { return new PathMoverGeneric(this); } @Override public boolean isClosedPath() { return this.isClosedPath; } @Override public boolean isNumberValue() { return false; } @Override final public boolean isTraceable() { return true; } @Override final public boolean getTrace() { return this.trace; } // G.Sturr 2010-5-18 get/set spreadsheet trace not needed here /* * public void setSpreadsheetTrace(boolean spreadsheetTrace) { * this.spreadsheetTrace = spreadsheetTrace; } * * public boolean getSpreadsheetTrace() { return spreadsheetTrace; } */ @Override public void setTrace(boolean trace) { this.trace = trace; } /** * Calculates the Cartesian coordinates of this curve for the given * parameter paramVal. The result is written to out. */ @Override public void evaluateCurve(double paramVal, double[] out) { out[0] = getFun(0).value(paramVal); out[1] = getFun(1).value(paramVal); } @Override public GeoVec2D evaluateCurve(double t) { return new GeoVec2D(this.kernel, getFun(0).value(t), getFun(1).value(t)); } /** * Calculates curvature for curve: k(t) = (a'(t)b''(t)-a''(t)b'(t))/T^3, T = * sqrt(a'(t)^2+b'(t)^2) * * @author Victor Franco, Markus Hohenwarter */ @Override public double evaluateCurvature(double t) { Function f1X, f1Y, f2X, f2Y; f1X = getFun(0).getDerivative(1, true); f1Y = getFun(1).getDerivative(1, true); f2X = getFun(0).getDerivative(2, true); f2Y = getFun(1).getDerivative(2, true); if (f1X == null || f1Y == null || f2X == null || f2Y == null) { return Double.NaN; } double f1eval[] = new double[2]; double f2eval[] = new double[2]; f1eval[0] = f1X.value(t); f1eval[1] = f1Y.value(t); f2eval[0] = f2X.value(t); f2eval[1] = f2Y.value(t); double t1 = Math.sqrt(f1eval[0] * f1eval[0] + f1eval[1] * f1eval[1]); double t3 = t1 * t1 * t1; return (f1eval[0] * f2eval[1] - f2eval[0] * f1eval[1]) / t3; } @Override public boolean isCasEvaluableObject() { return true; } @Override final public boolean isFunctionInX() { return false; } // Michael Borcherds 2008-04-30 @Override final public boolean isEqual(GeoElementND geo) { // TODO check for equality? return false; // if (geo.isGeoCurveCartesian()) return xxx; else return false; } @Override public boolean isFillable() { return true; } @Override public boolean isInverseFillable() { return isFillable(); } @Override final public void mirror(GeoConic c) { if (c.getType() == GeoConicNDConstants.CONIC_CIRCLE) { // Mirror point in circle double r = c.getHalfAxes()[0]; GeoVec2D midpoint = c.getTranslationVector(); double a = midpoint.getX(); double b = midpoint.getY(); this.translate(-a, -b); ExpressionNode exprX = getFun(0).deepCopy(this.kernel) .getExpression(); ExpressionNode exprY = getFun(1).deepCopy(this.kernel) .getExpression(); MyDouble d2 = new MyDouble(this.kernel, 2); ExpressionNode sf = new ExpressionNode(this.kernel, new MyDouble(this.kernel, r * r), Operation.DIVIDE, exprX.power(d2).plus(exprY.power(d2))); ExpressionNode transX = exprX.multiply(sf); ExpressionNode transY = exprY.multiply(sf); getFun(0).setExpression(transX); getFun(1).setExpression(transY); this.translate(a, b); } else { setUndefined(); } } /* * gets shortest distance to point p overridden in eg GeoPoint, GeoLine for * compound paths */ @Override public double distance(GeoPoint p) { double t = getClosestParameter(p, 0); return MyMath.length(getFun(0).value(t) - p.getX(), getFun(1).value(t) - p.getY()); } @Override public double distance(GeoPointND p) { if (!p.isGeoElement3D()) { return distance((GeoPoint) p); } double t = getClosestParameter(p, 0); Coords coords = p.getInhomCoordsInD3(); return MyMath.length(getFun(0).value(t) - coords.getX(), getFun(1).value(t) - coords.getY(), coords.getZ()); } @Override public void matrixTransform(double a00, double a01, double a02, double a10, double a11, double a12, double a20, double a21, double a22) { MyDouble ma00 = new MyDouble(this.kernel, a00); MyDouble ma01 = new MyDouble(this.kernel, a01); MyDouble ma02 = new MyDouble(this.kernel, a02); MyDouble ma10 = new MyDouble(this.kernel, a10); MyDouble ma11 = new MyDouble(this.kernel, a11); MyDouble ma12 = new MyDouble(this.kernel, a12); MyDouble ma20 = new MyDouble(this.kernel, a20); MyDouble ma21 = new MyDouble(this.kernel, a21); MyDouble ma22 = new MyDouble(this.kernel, a22); ExpressionNode exprX = getFun(0).deepCopy(this.kernel).getExpression(); ExpressionNode exprY = getFun(1).deepCopy(this.kernel).getExpression(); ExpressionNode transX = exprX.multiply(ma00).plus(exprY.multiply(ma01)) .plus(ma02); ExpressionNode transY = exprX.multiply(ma10).plus(exprY.multiply(ma11)) .plus(ma12); ExpressionNode transZ = exprX.multiply(ma20).plus(exprY.multiply(ma21)) .plus(ma22); getFun(0).setExpression(new ExpressionNode(this.kernel, transX, Operation.DIVIDE, transZ)); getFun(1).setExpression(new ExpressionNode(this.kernel, transY, Operation.DIVIDE, transZ)); } /** * @return x-function */ public Function getFunX() { return getFun(0); } /** * @return y-function */ public Function getFunY() { return getFun(1); } @Override public boolean hasDrawable3D() { return true; } @Override public double[] newDoubleArray() { return new double[2]; } @Override public double[] getDefinedInterval(double a, double b) { double[] intervalX = RealRootUtil .getDefinedInterval(getUnivariateFunctionX(), a, b); double[] intervalY = RealRootUtil .getDefinedInterval(getUnivariateFunctionY(), a, b); if (intervalX[0] < intervalY[0]) { intervalX[0] = intervalY[0]; } if (intervalX[1] > intervalY[1]) { intervalX[1] = intervalY[1]; } return intervalX; } @Override public double distanceMax(double[] p1, double[] p2) { return Math.max(Math.abs(p1[0] - p2[0]), Math.abs(p1[1] - p2[1])); } @Override protected GeoCurveCartesianND newGeoCurveCartesian(Construction cons1) { return new GeoCurveCartesian(cons1); } @Override final public HitType getLastHitType() { return HitType.ON_BOUNDARY; } @Override public boolean hasLineOpacity() { return true; } @Override public ValueType getValueType() { return ValueType.PARAMETRIC2D; } }