/* 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.euclidian.draw; import java.util.ArrayList; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.awt.GLine2D; import org.geogebra.common.awt.GPoint; import org.geogebra.common.awt.GPoint2D; import org.geogebra.common.awt.GRectangle; import org.geogebra.common.awt.GShape; import org.geogebra.common.euclidian.BoundingBox; import org.geogebra.common.euclidian.Drawable; import org.geogebra.common.euclidian.EuclidianView; import org.geogebra.common.euclidian.plot.CurvePlotter; import org.geogebra.common.euclidian.plot.GeneralPathClippedForCurvePlotter; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.VarString; import org.geogebra.common.kernel.advanced.AlgoFunctionInvert; import org.geogebra.common.kernel.arithmetic.ExpressionNode; import org.geogebra.common.kernel.arithmetic.ExpressionValue; import org.geogebra.common.kernel.arithmetic.FunctionVariable; import org.geogebra.common.kernel.arithmetic.Inspecting; import org.geogebra.common.kernel.arithmetic.ListValue; import org.geogebra.common.kernel.arithmetic.MyDouble; import org.geogebra.common.kernel.arithmetic.MyNumberPair; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoFunction; import org.geogebra.common.kernel.kernelND.CurveEvaluable; import org.geogebra.common.plugin.EuclidianStyleConstants; import org.geogebra.common.plugin.Operation; import org.geogebra.common.util.MyMath; import org.geogebra.common.util.debug.Log; /** * Draws graphs of parametric curves and functions * * @author Markus Hohenwarter, with ideas from John Gillam (see below) */ public class DrawParametricCurve extends Drawable { private CurveEvaluable curve; private GeneralPathClippedForCurvePlotter gp; private boolean isVisible, labelVisible, fillCurve; /** * Creates graphical representation of the curve * * @param view * Euclidian view in which it should be drawn * @param curve * Curve to be drawn */ public DrawParametricCurve(EuclidianView view, CurveEvaluable curve) { this.view = view; this.curve = curve; geo = curve.toGeoElement(); update(); } private StringBuilder labelSB = new StringBuilder(); @Override final public void update() { isVisible = geo.isEuclidianVisible(); if (!isVisible) { return; } dataExpression = null; if (geo.getLineType() == EuclidianStyleConstants.LINE_TYPE_POINTWISE && (curve instanceof GeoFunction)) { ((GeoFunction) curve).getFunctionExpression() .inspect(checkPointwise()); } labelVisible = geo.isLabelVisible(); updateStrokes(geo); if (dataExpression != null) { updatePointwise(); return; } if (gp == null) { gp = new GeneralPathClippedForCurvePlotter(view); } gp.reset(); fillCurve = filling(curve); double min = curve.getMinParameter(); double max = curve.getMaxParameter(); if (curve.toGeoElement().isGeoFunction()) { double minView = view.getXmin(); double maxView = view.getXmax(); if (min < minView || Double.isInfinite(min)) { min = minView; } if (max > maxView || Double.isInfinite(max)) { max = maxView; } } GPoint labelPoint; if (Kernel.isEqual(min, max)) { double[] eval = new double[2]; curve.evaluateCurve(min, eval); view.toScreenCoords(eval); labelPoint = new GPoint((int) eval[0], (int) eval[1]); } else { labelPoint = CurvePlotter.plotCurve(curve, min, max, view, gp, labelVisible, fillCurve ? CurvePlotter.Gap.CORNER : CurvePlotter.Gap.MOVE_TO); } // gp on screen? if (!gp.intersects(0, 0, view.getWidth(), view.getHeight())) { isVisible = false; // don't return here to make sure that getBounds() works for // offscreen points too } if (labelPoint != null) { xLabel = labelPoint.x; yLabel = labelPoint.y; switch (geo.getLabelMode()) { case GeoElement.LABEL_NAME_VALUE: StringTemplate tpl = StringTemplate.latexTemplate; labelSB.setLength(0); labelSB.append('$'); labelSB.append(geo.getLabel(tpl)); labelSB.append('('); labelSB.append(((VarString) geo).getVarString(tpl)); labelSB.append(")\\;=\\;"); labelSB.append(geo.getLaTeXdescription()); labelSB.append('$'); labelDesc = labelSB.toString(); break; case GeoElement.LABEL_VALUE: labelSB.setLength(0); labelSB.append('$'); labelSB.append(geo.getLaTeXdescription()); labelSB.append('$'); labelDesc = labelSB.toString(); break; case GeoElement.LABEL_CAPTION: default: // case LABEL_NAME: labelDesc = geo.getLabelDescription(); } addLabelOffsetEnsureOnScreen(view.getFontConic()); } // shape for filling if (geo.isInverseFill()) { setShape(AwtFactory.getPrototype().newArea(view.getBoundingPath())); getShape().subtract(AwtFactory.getPrototype().newArea(gp)); } // draw trace if (curve.getTrace()) { isTracing = true; GGraphics2D g2 = view.getBackgroundGraphics(); if (g2 != null) { drawTrace(g2); } } else { if (isTracing) { isTracing = false; // view.updateBackground(); } } } private int nPoints = 0; private ArrayList<GPoint2D> points; private GLine2D diag1, diag2; private void updatePointwise() { if (points == null) { points = new ArrayList<GPoint2D>(); } diag1 = AwtFactory.getPrototype().newLine2D(); int size = geo.getLineThickness(); diag1.setLine(-size, -size, size, size); diag2 = AwtFactory.getPrototype().newLine2D(); diag2.setLine(-size, size, size, -size); nPoints = 0; ListValue lvX = (ListValue) ((MyNumberPair) dataExpression.getRight()) .getX(); /* * ListValue lvY = (ListValue) ((MyNumberPair) * dataExpression.getRight()) .getY(); */ for (int i = 0; i < lvX.size(); i++) { double xRW = lvX.getListElement(i).evaluateDouble(); if (invert != null) { invFV.set(xRW); xRW = invert.evaluateDouble(); } double x = view.toScreenCoordXd(xRW); if (x < 0 || x > view.getWidth()) { continue; } double y = view .toScreenCoordYd(((GeoFunction) curve).value(xRW)); if (y < 0 || y > view.getHeight()) { continue; } GPoint2D pt = AwtFactory.getPrototype().newPoint2D(x, y); if (points.size() > nPoints) { points.set(nPoints, pt); } else { points.add(pt); } nPoints++; } } private ExpressionNode dataExpression; private FunctionVariable invFV; private ExpressionNode invert; private Inspecting checkPointwise() { return new Inspecting() { @Override public boolean check(ExpressionValue v) { /* * if (v.isExpressionNode() && ((ExpressionNode) * v).getOperation() == Operation.FUNCTION) { * if(((ExpressionNode) v).getLeft() instanceof GeoFunction && * ((GeoFunction)((ExpressionNode) * v).getLeft()).getFunctionExpression().inspect(this)){ return * true; } } */ if (v.isExpressionNode() && ((ExpressionNode) v) .getOperation() == Operation.DATA) { return updateDataExpression((ExpressionNode) v); } return false; } }; } /** * @param v * new node * @return whether this is a valid datafunction */ protected boolean updateDataExpression(ExpressionNode v) { dataExpression = (v); if (dataExpression.getLeft().unwrap() instanceof FunctionVariable) { invert = null; return true; } invFV = new FunctionVariable(view.getApplication().getKernel()); invert = AlgoFunctionInvert.invert(dataExpression.getLeft().unwrap(), ((GeoFunction) curve).getFunctionVariables()[0], invFV, geo.getKernel()); if (invert == null) { dataExpression = null; } return true; } @Override final public void draw(GGraphics2D g2) { if (isVisible) { if (dataExpression != null) { g2.setPaint(getObjectColor()); if (geo.doHighlighting()) { g2.setPaint(geo.getSelColor()); g2.setStroke(selStroke); drawPoints(g2); } g2.setStroke(objStroke); drawPoints(g2); return; } if (geo.doHighlighting()) { g2.setPaint(geo.getSelColor()); g2.setStroke(selStroke); g2.draw(gp); } g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(gp); if (fillCurve) { try { // fill using default/hatching/image as appropriate fill(g2, (geo.isInverseFill() ? getShape() : gp)); } catch (Exception e) { Log.error(e.getMessage()); } } if (labelVisible) { g2.setFont(view.getFontConic()); g2.setPaint(geo.getLabelColor()); drawLabel(g2); } } } private void drawPoints(GGraphics2D g2) { for (int i = 0; i < nPoints; i++) { g2.saveTransform(); g2.translate(points.get(i).getX(), points.get(i).getY()); g2.draw(diag1); g2.draw(diag2); g2.restoreTransform(); } } @Override protected final void drawTrace(GGraphics2D g2) { g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(gp); } @Override final public boolean hit(int x, int y, int hitThreshold) { if (isVisible) { if (dataExpression != null) { for (int i = 0; i < nPoints; i++) { if (MyMath.length(x - points.get(i).getX(), y - points.get(i).getY()) < hitThreshold) { return true; } } return false; } GShape t = geo.isInverseFill() ? getShape() : gp; if (strokedShape == null) { // strokedShape = new // geogebra.awt.GenericShape(geogebra.awt.BasicStroke.getAwtStroke(objStroke).createStrokedShape(geogebra.awt.GenericShape.getAwtShape(gp))); strokedShape = objStroke.createStrokedShape(gp); } if (geo.isFilled()) { return t.intersects(x - hitThreshold, y - hitThreshold, 2 * hitThreshold, 2 * hitThreshold); } // workaround for #2364 if (geo.isGeoFunction()) { GeoFunction f = (GeoFunction) geo; double rwx = view.toRealWorldCoordX(x); double low = view.toRealWorldCoordY(y + hitThreshold); double high = view.toRealWorldCoordY(y - hitThreshold); double dx = hitThreshold * view.getInvXscale(); double left = f.value(rwx - dx); if (left >= low && left <= high) { return true; } double right = f.value(rwx + dx); if (right >= low && right <= high) { return true; } double middle = f.value(rwx); if (middle >= low && middle <= high) { return true; } if ((right < low && left < low && middle < low) || (right > high && left > high && middle > high) || (!MyDouble.isFinite(right) && !MyDouble.isFinite(left) && !MyDouble.isFinite(middle))) { return false; } return gp.intersects(x - hitThreshold, y - hitThreshold, 2 * hitThreshold, 2 * hitThreshold) && !gp.contains(x - hitThreshold, y - hitThreshold, 2 * hitThreshold, 2 * hitThreshold); } // not GeoFunction, eg parametric return strokedShape.intersects(x - hitThreshold, y - hitThreshold, 2 * hitThreshold, 2 * hitThreshold); } return false; /* * return gp.intersects(x-3,y-3,6,6) && !gp.contains(x-3,y-3,6,6); */ } @Override public boolean intersectsRectangle(GRectangle rect) { if (isVisible) { GShape t = geo.isInverseFill() ? getShape() : gp; if (strokedShape == null) { // strokedShape = new // geogebra.awt.GenericShape(geogebra.awt.BasicStroke.getAwtStroke(objStroke).createStrokedShape(geogebra.awt.GenericShape.getAwtShape(gp))); strokedShape = objStroke.createStrokedShape(gp); } if (geo.isFilled()) { return t.intersects(rect); } return strokedShape.intersects(rect); } return false; } @Override final public boolean isInside(GRectangle rect) { return gp != null && rect.contains(gp.getBounds()); } @Override public GeoElement getGeoElement() { return geo; } @Override public void setGeoElement(GeoElement geo) { this.geo = geo; } /** * Returns the bounding box of this DrawPoint in screen coordinates. */ @Override final public GRectangle getBounds() { if (!geo.isDefined() || !curve.isClosedPath() || !geo.isEuclidianVisible()) { return null; } return AwtFactory.getPrototype().newRectangle(gp.getBounds()); } final private static boolean filling(CurveEvaluable curve) { return !curve.isFunctionInX() && curve.toGeoElement().isFilled(); } @Override public BoundingBox getBoundingBox() { // TODO Auto-generated method stub return null; } @Override public void updateBoundingBox() { // TODO Auto-generated method stub } }