package org.geogebra.common.kernel.commands;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeSet;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.algos.AlgoCurveCartesian;
import org.geogebra.common.kernel.algos.AlgoDependentNumber;
import org.geogebra.common.kernel.arithmetic.Equation;
import org.geogebra.common.kernel.arithmetic.ExpressionNode;
import org.geogebra.common.kernel.arithmetic.ExpressionValue;
import org.geogebra.common.kernel.arithmetic.Function;
import org.geogebra.common.kernel.arithmetic.FunctionNVar;
import org.geogebra.common.kernel.arithmetic.FunctionVariable;
import org.geogebra.common.kernel.arithmetic.MyDouble;
import org.geogebra.common.kernel.arithmetic.MyList;
import org.geogebra.common.kernel.arithmetic.NumberValue;
import org.geogebra.common.kernel.arithmetic.Traversing;
import org.geogebra.common.kernel.arithmetic.Traversing.CollectUndefinedVariables;
import org.geogebra.common.kernel.arithmetic.Traversing.VariableReplacer;
import org.geogebra.common.kernel.arithmetic.ValidExpression;
import org.geogebra.common.kernel.arithmetic.Variable;
import org.geogebra.common.kernel.arithmetic.VectorArithmetic;
import org.geogebra.common.kernel.arithmetic.VectorValue;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoNumberValue;
import org.geogebra.common.kernel.geos.GeoNumeric;
import org.geogebra.common.kernel.kernelND.GeoConicND;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.kernel.kernelND.GeoLineND;
import org.geogebra.common.main.MyError;
import org.geogebra.common.plugin.Operation;
import org.geogebra.common.util.AsyncOperation;
import org.geogebra.common.util.debug.Log;
import org.geogebra.common.util.lang.Unicode;
/**
* Processing
*
* f(a)=(a,a) X=(a,a) (t,t)
*
*/
public class ParametricProcessor {
/**
* kernel
*/
protected Kernel kernel;
/**
* Algebra processor
*/
protected AlgebraProcessor ap;
/**
* @param kernel
* kernel
* @param ap
* algebra processor
*/
public ParametricProcessor(Kernel kernel, AlgebraProcessor ap) {
this.kernel = kernel;
this.ap = ap;
}
/**
* @param ve0
* expression; should have label X or contain t, theta (also
* equations X=... handled here for some reason)
* @param undefinedVariables
* list of names of undefined variables
* @param autocreateSliders
* whether to create sliders
* @param callback
* call this after sliders are created
* @param info
* processing information
* @return resulting elements
*/
final GeoElement[] checkParametricEquation(ValidExpression ve0,
TreeSet<String> undefinedVariables,
AsyncOperation<GeoElementND[]> callback, EvalInfo info) {
if (undefinedVariables.isEmpty()) {
return null;
}
boolean parametricExpression = ("X".equals(ve0.getLabel())
|| undefinedVariables.contains("t"));
boolean parametricEquation = ve0.unwrap() instanceof Equation
&& "X".equals(((Equation) ve0.unwrap()).getLHS()
.toString(StringTemplate.defaultTemplate));
if (!parametricEquation && !parametricExpression) {
return null;
}
GeoElement replaceable = ap.getReplaceable(ve0.getLabels());
Construction cons = kernel.getConstruction();
boolean oldMacroMode = cons.isSuppressLabelsActive();
if (replaceable != null) {
cons.setSuppressLabelCreation(true);
}
String varName = getPreferredName(undefinedVariables);
ValidExpression ve = ve0;
TreeSet<GeoNumeric> num = new TreeSet<GeoNumeric>();
ap.replaceUndefinedVariables(ve, num, new String[] { varName, "X" });// Iteration[a+1,
// a,
// {1},4]
for (GeoNumeric slider : num) {
undefinedVariables.remove(slider.getLabelSimple());
}
if (parametricExpression) {
try {
FunctionVariable fv = new FunctionVariable(kernel, varName);
ExpressionNode exp = ve.deepCopy(kernel).traverse(
VariableReplacer.getReplacer(varName, fv, kernel))
.wrap();
exp.resolveVariables(new EvalInfo(false));
GeoElement[] ret = processParametricFunction(exp,
exp.evaluate(StringTemplate.defaultTemplate),
new FunctionVariable[] { fv },
"X".equals(ve.getLabel()) ? null : ve.getLabel(), info);
if (ret != null
&& (num.isEmpty() || info.isAutocreateSliders())) {
cons.setSuppressLabelCreation(oldMacroMode);
ap.processReplace(replaceable, ret, null, info);
return ret;
}
} catch (Throwable tt) {
Log.debug("X is not parametric:" + tt.getMessage());
}
removeSliders(num, undefinedVariables);
} else if (parametricEquation) {
// TODO this branch should be handled by processEquation, so maybe
// not needed at all
try {
FunctionVariable fv = new FunctionVariable(kernel, varName);
ExpressionNode exp = ((Equation) ve.unwrap()).getRHS()
.deepCopy(kernel).traverse(VariableReplacer
.getReplacer(varName, fv, kernel))
.wrap();
exp.resolveVariables(info);
GeoElement[] ret = processParametricFunction(exp,
exp.evaluate(StringTemplate.defaultTemplate),
new FunctionVariable[] { fv }, ve.getLabel(), info);
if (ret != null
&& (num.isEmpty() || info.isAutocreateSliders())) {
cons.setSuppressLabelCreation(oldMacroMode);
ap.processReplace(replaceable, ret, null, info);
return ret;
}
} catch (Throwable tt) {
tt.printStackTrace();
Log.debug("X is not parametric");
}
removeSliders(num, undefinedVariables);
}
removeSliders(num, undefinedVariables);
cons.setSuppressLabelCreation(oldMacroMode);
return null;
}
private static String getPreferredName(TreeSet<String> undefinedVariables) {
if (undefinedVariables.contains("t")) {
return "t";
}
if (undefinedVariables.contains(Unicode.thetaStr)) {
return Unicode.thetaStr;
}
Iterator<String> t = undefinedVariables.iterator();
String varName = t.next();
if ("X".equals(varName)) {
if (t.hasNext()) {
return t.next();
}
return "t";
} else if ("y".equals(varName)) {
return Unicode.lambdaStr;
}
return varName;
}
private static void removeSliders(TreeSet<GeoNumeric> num,
TreeSet<String> undefined) {
for (GeoNumeric slider : num) {
slider.remove();
undefined.add(slider.getLabelSimple());
}
}
/**
* @param ev
* expression
* @return input expression or zero expression if input is null
*/
protected ExpressionNode expr(ExpressionValue ev) {
if (ev == null) {
return new ExpressionNode(kernel, 0);
}
return ev.wrap();
}
/**
* @param exp
* expression
* @param ev
* evaluated exp
* @param fv
* function variable
* @param label
* label for output
* @param info
* processing information
* @return paramteric curve (or line, conic)
*/
protected GeoElement[] processParametricFunction(ExpressionNode exp,
ExpressionValue ev, FunctionVariable[] fv, String label,
EvalInfo info) {
Construction cons = kernel.getConstruction();
if (ev instanceof MyList) {
GeoElement ge = kernel.getAlgebraProcessor().listExpression(exp);
ge.setLabel(exp.getLabel());
return ge.asArray();
}
if (fv.length < 2 && ev instanceof VectorValue
&& ((VectorValue) ev).getMode() != Kernel.COORD_COMPLEX) {
GeoNumeric locVar = getLocalVar(exp, fv[0]);
if (exp.getOperation() == Operation.IF) {
ExpressionNode exp1 = exp.getRightTree();
ExpressionNode cx = VectorArithmetic.computeCoord(exp1, 0);
ExpressionNode cy = VectorArithmetic.computeCoord(exp1, 1);
return cartesianCurve(cons, label, exp1, locVar,
new ExpressionNode[] { cx, cy },
exp.getLeftTree());
}
ExpressionNode cx = VectorArithmetic.computeCoord(exp, 0);
ExpressionNode cy = VectorArithmetic.computeCoord(exp, 1);
ExpressionValue[] coefX = new ExpressionValue[5],
coefY = new ExpressionValue[5];
if (ap.getTrigCoeffs(cx, coefX, new ExpressionNode(kernel, 1.0),
locVar)
&& ap.getTrigCoeffs(cy, coefY,
new ExpressionNode(kernel, 1.0), locVar)) {
ExpressionNode a, b, c, d, xx, xy, yy;
ExpressionNode x = new FunctionVariable(kernel, "x").wrap()
.subtract(expr(coefX[0]));
ExpressionNode y = new FunctionVariable(kernel, "y").wrap()
.subtract(expr(coefY[0]));
if (coefX[1] != null || coefX[2] != null) {
a = expr(coefX[1]);
b = expr(coefX[2]);
c = expr(coefY[1]);
d = expr(coefY[2]);
xx = c.power(2).plus(d.power(2)).multiply(x).multiply(x);
xy = c.multiply(a).plus(d.multiply(b)).multiply(-2)
.multiply(x).multiply(y);
yy = a.power(2).plus(b.power(2)).multiply(y).multiply(y);
} else {
a = expr(coefX[3]);
b = expr(coefX[4]);
c = expr(coefY[3]);
d = expr(coefY[4]);
xx = c.power(2).subtract(d.power(2)).multiply(x)
.multiply(x);
xy = c.multiply(a).subtract(d.multiply(b)).multiply(-2)
.multiply(x).multiply(y);
yy = a.power(2).subtract(b.power(2)).multiply(y)
.multiply(y);
}
ExpressionNode den = a.power(2).multiply(d.power(2))
.plus(b.power(2).multiply(c.power(2)))
.subtract(a.multiply(b).multiply(c).multiply(d)
.multiply(2));
Equation eq = new Equation(kernel, xx.plus(xy).plus(yy).wrap(),
den);
return paramConic(eq, exp, label, fv[0].getSetVarString());
}
coefX = ap.arrayOfZeros(coefX.length);
coefY = ap.arrayOfZeros(coefY.length);
int degX = ap.getPolyCoeffs(cx, coefX,
new ExpressionNode(kernel, 1.0), locVar);
int degY = ap.getPolyCoeffs(cy, coefY,
new ExpressionNode(kernel, 1.0), locVar);
// line
if ((degX >= 0 && degY >= 0) && (degX < 2 && degY < 2)) {
FunctionVariable px = new FunctionVariable(kernel, "x");
FunctionVariable py = new FunctionVariable(kernel, "y");
Equation eq = new Equation(kernel,
coefX[1].wrap().multiply(py)
.subtract(coefY[1].wrap().multiply(px)),
coefX[1].wrap().multiply(coefY[0])
.subtract(coefX[0].wrap().multiply(coefY[1])));
eq.setForceLine();
eq.initEquation();
eq.setLabel(label);
GeoElement[] line = ap.processLine(eq, buildParamEq(exp));
((GeoLineND) line[0]).setToParametric(fv[0].getSetVarString());
line[0].update();
return line;
// parabola
// x=att+bt+c
// y=dtt+et+f
// t=(d*x-a*y-d*c+a*f)/(d*b-a*e)
} else if (degX >= 0 && degY >= 0) {
FunctionVariable px = new FunctionVariable(kernel, "x");
FunctionVariable py = new FunctionVariable(kernel, "y");
Log.debug(coefX[2] + "," + coefX[1] + "," + coefX[0]);
ExpressionNode t = px.wrap().multiply(coefY[2])
.subtract(py.wrap().multiply(coefX[2]))
.plus(coefX[2].wrap().multiply(coefY[0]))
.subtract(coefY[2].wrap().multiply(coefX[0]));
ExpressionNode d = coefX[1].wrap().multiply(coefY[2])
.subtract(coefY[1].wrap().multiply(coefX[2]));
Equation eq;
// Numerically unstable
eq = new Equation(kernel,
d.power(2).multiply(px).multiply(coefX[2])
.plus(d.power(2).multiply(py)
.multiply(coefY[2])),
t.power(2)
.multiply(coefY[2].wrap().power(2)
.plus(coefX[2].wrap().power(2)))
.plus(t.multiply(
coefY[1].wrap().multiply(coefY[2])
.plus(coefX[1].wrap()
.multiply(coefX[2])))
.multiply(d))
.plus(d.power(2).multiply(coefY[0].wrap()
.multiply(coefY[2]).plus(coefX[0].wrap()
.multiply(coefX[2])))));
return paramConic(eq, exp, label, fv[0].getSetVarString());
}
return cartesianCurve(cons, label, exp, locVar,
new ExpressionNode[] { cx, cy }, null);
} else if (ev instanceof Function) {
return ap.processFunction((Function) ev, info);
} else if (ev instanceof FunctionNVar) {
return ap.processFunctionNVar((FunctionNVar) ev, info);
}
Log.debug("InvalidFunction:"
+ exp.toString(StringTemplate.defaultTemplate) + ","
+ ev.getClass() + "," + fv.length);
throw new MyError(kernel.getApplication().getLocalization(),
"InvalidFunction");
}
/**
* @param cons
* construction
* @param label
* label
* @param exp
* expression
* @param locVar
* variable
* @param c
* coordinates
* @param condition
* limitation for input variable
* @return curve
*/
protected GeoElement[] cartesianCurve(Construction cons, String label,
ExpressionNode exp, GeoNumeric locVar, ExpressionNode[] c,
ExpressionNode condition) {
for (int i = 0; i < c.length; i++) {
checkNumber(c[i]);
}
GeoNumberValue[] coords = new GeoNumberValue[c.length];
for (int i = 0; i < c.length; i++) {
AlgoDependentNumber nx = new AlgoDependentNumber(cons, c[i], false);
cons.removeFromConstructionList(nx);
coords[i] = nx.getNumber();
}
GeoNumberValue from = null, to = null;
if (condition != null) {
from = getBound(locVar, condition, true);
to = getBound(locVar, condition, false);
}
boolean trig = c[0].has2piPeriodicOperations();
if (from == null) {
from = new GeoNumeric(cons, trig ? 0 : -10);
}
if (to == null) {
to = trig ? piTimes(2, cons) : new GeoNumeric(cons, 10);
}
AlgoCurveCartesian ac = makeCurveAlgo(cons,
exp.deepCopy(kernel).wrap(),
coords, locVar,
from, to);
ac.getCurve().setLabel(label);
return ac.getOutput();
}
/**
* @param cons
* construction
* @param wrap
* expression
* @param coords
* coords
* @param locVar
* variable
* @param from
* min
* @param to
* max
* @return curve algo
*/
protected AlgoCurveCartesian makeCurveAlgo(Construction cons,
ExpressionNode wrap, GeoNumberValue[] coords, GeoNumeric locVar,
GeoNumberValue from, GeoNumberValue to) {
return new AlgoCurveCartesian(cons, wrap, coords, locVar, from, to);
}
/**
* @param cx
* potential numeric expression
*/
protected void checkNumber(ExpressionNode cx) {
if (!cx.evaluate(StringTemplate.maxPrecision).isNumberValue()) {
throw new MyError(kernel.getApplication().getLocalization(),
"InvalidFunction");
}
}
private GeoNumberValue getBound(GeoNumeric locVar, ExpressionNode condition,
boolean swap) {
if (condition.getOperation() == Operation.AND
|| condition.getOperation() == Operation.AND_INTERVAL) {
GeoNumberValue lt = getBound(locVar, condition.getLeftTree(), swap);
if (lt != null) {
return lt;
}
GeoNumberValue rt = getBound(locVar, condition.getRightTree(),
swap);
if (rt != null) {
return rt;
}
}
ExpressionValue checkVar = swap ? condition.getLeft()
: condition.getRight();
ExpressionValue checkNum = !swap ? condition.getLeft()
: condition.getRight();
if ((condition.getOperation() == Operation.GREATER
|| condition.getOperation() == Operation.GREATER_EQUAL)
&& checkVar == locVar) {
return (GeoNumberValue) ap.processNumber(checkNum.wrap(),
checkNum.evaluate(StringTemplate.defaultTemplate),
new EvalInfo(false))[0];
}
if ((condition.getOperation() == Operation.LESS
|| condition.getOperation() == Operation.LESS_EQUAL)
&& checkNum == locVar) {
return (GeoNumberValue) ap.processNumber(checkVar.wrap(),
checkVar.evaluate(StringTemplate.defaultTemplate),
new EvalInfo(false))[0];
}
return null;
}
private GeoNumberValue piTimes(int i, Construction cons) {
ExpressionNode en = new ExpressionNode(kernel, new MyDouble(kernel, i),
Operation.MULTIPLY, new MyDouble(kernel, Math.PI));
GeoNumeric ret = new GeoNumeric(cons, i * Math.PI);
ret.setDefinition(en);
return ret;
}
/**
* @param exp
* RHS of parametric equation
* @return equation X = exp
*/
protected ExpressionNode buildParamEq(ExpressionNode exp) {
return new Equation(kernel, new Variable(kernel, "X"), exp).wrap();
}
private GeoElement[] paramConic(Equation eq, ExpressionNode exp,
String label, String param) {
eq.initEquation();
eq.setForceConic();
eq.setLabel(label);
GeoElement[] ret = ap.processConic(eq, buildParamEq(exp));
((GeoConicND) ret[0]).toParametric(param);
((GeoConicND) ret[0]).update();
return ret;
}
/**
* Creates a number and replaces all occurences of the variable with it
*
* @param exp
* expression
* @param fv
* function variable
* @return numeric that replaces the variable
*/
protected GeoNumeric getLocalVar(ExpressionNode exp, FunctionVariable fv) {
GeoNumeric locVar = new GeoNumeric(kernel.getConstruction());
locVar.setSendValueToCas(false);
locVar.setLocalVariableLabel(fv.getSetVarString());
exp.replace(fv, locVar);
return locVar;
}
/**
* @param ve
* expression that might be RHS of parametric equation, eg (t,t)
* @param fallback
* what to return if ve is not parametric equation
* @param cons
* construction
* @param info
* processing information
* @return parametric curve (or line, conic) or fallback
*/
public ValidExpression checkParametricEquationF(ValidExpression ve,
ValidExpression fallback, Construction cons, EvalInfo info) {
CollectUndefinedVariables collecter = new Traversing.CollectUndefinedVariables();
ve.traverse(collecter);
final TreeSet<String> undefinedVariables = collecter.getResult();
if (undefinedVariables.size() == 1) {
try {
String varName = undefinedVariables.first();
FunctionVariable fv = new FunctionVariable(kernel, varName);
ExpressionNode exp = ve.deepCopy(kernel).traverse(
VariableReplacer.getReplacer(varName, fv, kernel))
.wrap();
exp.resolveVariables(info);
boolean flag = cons.isSuppressLabelsActive();
cons.setSuppressLabelCreation(true);
GeoElement[] ret = processParametricFunction(exp,
exp.evaluate(StringTemplate.defaultTemplate),
new FunctionVariable[] { fv }, null, info);
cons.setSuppressLabelCreation(flag);
if (ret != null) {
return ret[0].wrap();
}
} catch (Throwable t) {
t.printStackTrace();
Log.debug("X is not parametric");
}
}
return fallback;
}
/**
* @param equ
* aquation with X on LHS
* @param info
* processing information
* @return parametric curve if possible
*/
public GeoElement[] processXEquation(Equation equ, EvalInfo info) {
CollectUndefinedVariables collecter = new Traversing.CollectUndefinedVariables();
equ.traverse(collecter);
final TreeSet<String> undefinedVariables = collecter.getResult();
// case 3DLine
if (undefinedVariables.isEmpty()) {
undefinedVariables.add("y");
}
String varName = getPreferredName(undefinedVariables);
FunctionVariable fv = new FunctionVariable(kernel, varName);
ExpressionNode exp = equ.getRHS().deepCopy(kernel)
.traverse(VariableReplacer.getReplacer(varName, fv, kernel))
.wrap();
exp.resolveVariables(info);
GeoElement[] ret = processParametricFunction(exp,
exp.evaluate(StringTemplate.defaultTemplate),
new FunctionVariable[] { fv }, equ.getLabel(), info);
return ret;
}
/**
* Returns the single free GeoNumeric/MyDouble expression wrapped in this
* ExpressionValue. For "a + x(A)" this returns a, for "x(A)" this returns
* null where A is a free point. If A is a dependent point, "a + x(A)"
* throws an Exception.
*
* @param ev
* expression
* @return number
*/
public NumberValue getCoordNumber(ExpressionValue ev) {
// simple variable "a"
if (ev.isLeaf()) {
// handle (a,1) and (1,a) case
// 1 is MyDouble
if (ev.isExpressionNode()) {
if (((ExpressionNode) ev).getLeft() instanceof MyDouble) {
return (NumberValue) ((ExpressionNode) ev).getLeft();
}
}
GeoElement geo = kernel.lookupLabel(ev.isGeoElement()
? ((GeoElement) ev).getLabel(StringTemplate.defaultTemplate)
: ev.toString(StringTemplate.defaultTemplate));
if (geo != null && geo.isGeoNumeric()
&& geo.isPointerChangeable()) {
return (GeoNumeric) geo;
}
return null;
}
// return value
GeoNumeric coordNumeric = null;
// expression + expression
ExpressionNode en = (ExpressionNode) ev;
if (en.getOperation().equals(Operation.PLUS)
&& en.getLeft() instanceof GeoNumeric) {
// left branch needs to be a single number variable: get it
// e.g. a + x(D)
coordNumeric = (GeoNumeric) en.getLeft();
if (!coordNumeric.isChangeable()) {
return null;
}
// check that variables in right branch are all independent to avoid
// circular definitions
HashSet<GeoElement> rightVars = en.getRight().getVariables();
if (rightVars != null) {
Iterator<GeoElement> it = rightVars.iterator();
while (it.hasNext()) {
GeoElement var = it.next();
if (var.isChildOrEqual(coordNumeric)) {
return null;
}
}
}
}
return coordNumeric;
}
}