/* 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.GArc2D; import org.geogebra.common.awt.GEllipse2DDouble; import org.geogebra.common.awt.GGeneralPath; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.awt.GLine2D; 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.Previewable; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.Construction; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.Matrix.Coords; import org.geogebra.common.kernel.algos.AlgoAngle; import org.geogebra.common.kernel.algos.AlgoAnglePoints; import org.geogebra.common.kernel.geos.GeoAngle; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoPoint; import org.geogebra.common.kernel.kernelND.GeoPointND; import org.geogebra.common.plugin.EuclidianStyleConstants; import org.geogebra.common.util.debug.Log; /** * * @author Markus Hohenwarter, Loic De Coq */ public class DrawAngle extends Drawable implements Previewable { private GeoAngle angle; private boolean isVisible, labelVisible, show90degrees; private AlgoAngle algo; // private Arc2D.Double fillArc = new Arc2D.Double(); private GArc2D drawArc = AwtFactory.getPrototype().newArc2D(); private GGeneralPath polygon = AwtFactory.getPrototype().newGeneralPath(); // Michael // Borcherds // 2007-11-19 private GEllipse2DDouble dot90degree; private GShape shape; private double m[] = new double[2]; private double coords[] = new double[2]; private double[] firstVec = new double[2]; private boolean drawDot; private GeoPoint[] previewTempPoints; // For decoration private GShape shapeArc1, shapeArc2; private GArc2D decoArc = AwtFactory.getPrototype().newArc2D(); private GLine2D[] tick; private double[] angleTick = new double[2]; /** maximum angle distance between two ticks. */ public static final double MAX_TICK_DISTANCE = Math.toRadians(15); private GGeneralPath square; private ArrayList<GeoPointND> prevPoints; private double maxRadius; // END /** * @param view * Euclidian view * @param angle * Angle to be drawn */ public DrawAngle(EuclidianView view, GeoAngle angle) { this.view = view; this.angle = angle; geo = angle; init(); if (algo != null) { update(); } } /** * Creates a new DrawAngle for preview * * @param view * view * @param points * list of points */ public DrawAngle(EuclidianView view, ArrayList<GeoPointND> points) { this.view = view; prevPoints = points; Construction cons = view.getKernel().getConstruction(); previewTempPoints = new GeoPoint[3]; for (int i = 0; i < previewTempPoints.length; i++) { previewTempPoints[i] = new GeoPoint(cons); } initPreview(); } private void init() { firstVec = new double[] { 1, 0 }; m = new double[] { 0, 0 }; if (angle.getDrawAlgorithm() instanceof AlgoAngle) { algo = ((AlgoAngle) angle.getDrawAlgorithm()); } } /** * * @param pt * point * @return true if coords are in this view */ public boolean inView(Coords pt) { return true; } /** * * @param p * point * @return coords of the point in view */ final public Coords getCoordsInView(GeoPointND p) { return getCoordsInView(p.getInhomCoordsInD3()); } /** * * @param p * point * @return coords of the point in view */ public Coords getCoordsInView(Coords p) { return p; } /** * Used for view from plane (may be reverse oriented) * * @param start * initial start * @param extent * angle extent * @return angle start */ protected double getAngleStart(double start, double extent) { return start; } private void setNotVisible() { isVisible = false; shape = null; labelVisible = false; } @Override final public void update() { if (!geo.getDrawAlgorithm().equals(geo.getParentAlgorithm())) { init(); } isVisible = true; if (!geo.isEuclidianVisible() || Kernel.isZero(angle.getValue())) { setNotVisible(); // we may return here; the object is not offscreen, but invisible. return; } labelVisible = geo.isLabelVisible(); updateStrokes(angle); maxRadius = Double.POSITIVE_INFINITY; // set vertex and first vector to determine start angle if (algo == null) { setNotVisible(); return; } if (!algo.updateDrawInfo(m, firstVec, this)) { setNotVisible(); return; } // calc start angle double angSt = Math.atan2(firstVec[1], firstVec[0]); if (Double.isNaN(angSt) || Double.isInfinite(angSt)) { setNotVisible(); return; } // Michael Borcherds 2007-11-19 BEGIN // double angExt = angle.getValue(); double angExt = angle.getRawAngle(); angSt = getAngleStart(angSt, angExt); // if this angle was not allowed to become a reflex angle // (i.e. greater than pi) we got (2pi - angleValue) for angExt // if (angle.changedReflexAngle()) { // angSt = angSt - angExt; // } switch (angle.getAngleStyle()) { case UNBOUNDED: Log.error("Unbounded angle shouldn't be drawable"); break; // case GeoAngle.ANGLE_ISCLOCKWISE: // angSt += angExt; // angExt = 2.0 * Math.PI - angExt; // break; case NOTREFLEX: if (angExt > Math.PI) { angSt += angExt; angExt = 2.0 * Math.PI - angExt; } break; case ISREFLEX: if (angExt < Math.PI) { angSt += angExt; angExt = 2.0 * Math.PI - angExt; } break; } // Michael Borcherds 2007-11-19 END double as = Math.toDegrees(angSt); double ae = Math.toDegrees(angExt); int arcSize = Math.min((int) maxRadius, angle.getArcSize()); double r = arcSize * view.getInvXscale(); // check whether we need to take care for a special 90 degree angle // appearance show90degrees = view .getRightAngleStyle() != EuclidianStyleConstants.RIGHT_ANGLE_STYLE_NONE && angle.isEmphasizeRightAngle() && Kernel.isEqual(angExt, Kernel.PI_HALF); // set coords to screen coords of vertex coords[0] = m[0]; coords[1] = m[1]; view.toScreenCoords(coords); // for 90 degree angle drawDot = false; // SPECIAL case for 90 degree angle, by Loic and Markus if (show90degrees) { switch (view.getRightAngleStyle()) { default: case EuclidianStyleConstants.RIGHT_ANGLE_STYLE_SQUARE: // set 90 degrees square if (square == null) { square = AwtFactory.getPrototype().newGeneralPath(); } else { square.reset(); } double length = arcSize * 0.7071067811865; square.moveTo(coords[0], coords[1]); square.lineTo((coords[0] + length * Math.cos(angSt)), (coords[1] - length * Math.sin(angSt) * view.getScaleRatio())); square.lineTo( (coords[0] + arcSize * Math.cos(angSt + Kernel.PI_HALF / 2)), (coords[1] - arcSize * Math.sin(angSt + Kernel.PI_HALF / 2) * view.getScaleRatio())); square.lineTo( (coords[0] + length * Math.cos(angSt + Kernel.PI_HALF)), (coords[1] - length * Math.sin(angSt + Kernel.PI_HALF) * view.getScaleRatio())); square.lineTo(coords[0], coords[1]); shape = square; break; case EuclidianStyleConstants.RIGHT_ANGLE_STYLE_L: // Belgian offset |_ if (square == null) { square = AwtFactory.getPrototype().newGeneralPath(); } else { square.reset(); } length = arcSize * 0.7071067811865; double offset = length * 0.4; square.moveTo( (coords[0] + length * Math.cos(angSt) + offset * Math.cos(angSt) + offset * Math.cos(angSt + Kernel.PI_HALF)), (coords[1] - length * Math.sin(angSt) * view.getScaleRatio() - offset * Math.sin(angSt) - offset * Math.sin(angSt + Kernel.PI_HALF))); square.lineTo( (coords[0] + offset * Math.cos(angSt) + offset * Math.cos(angSt + Kernel.PI_HALF)), (coords[1] - offset * Math.sin(angSt) - offset * Math.sin(angSt + Kernel.PI_HALF))); square.lineTo( (coords[0] + length * Math.cos(angSt + Kernel.PI_HALF) + offset * Math.cos(angSt) + offset * Math.cos(angSt + Kernel.PI_HALF)), (coords[1] - length * Math.sin(angSt + Kernel.PI_HALF) * view.getScaleRatio() - offset * Math.sin(angSt) - offset * Math.sin(angSt + Kernel.PI_HALF))); shape = square; // FIXME break; case EuclidianStyleConstants.RIGHT_ANGLE_STYLE_DOT: // set 90 degrees dot drawDot = true; if (dot90degree == null) { dot90degree = AwtFactory.getPrototype() .newEllipse2DDouble(); } int diameter = 2 * geo.getLineThickness(); double radius = r / 1.7; double labelAngle = angSt + angExt / 2.0; coords[0] = m[0] + radius * Math.cos(labelAngle); coords[1] = m[1] + radius * Math.sin(labelAngle); view.toScreenCoords(coords); dot90degree.setFrame(coords[0] - geo.getLineThickness(), coords[1] - geo.getLineThickness(), diameter, diameter); // set arc in real world coords and transform to screen coords drawArc.setArcByCenter(m[0], m[1], r, -as, -ae, GArc2D.PIE); shape = view.getCoordTransform() .createTransformedShape(drawArc); break; } } // STANDARE case: draw arc with possible decoration else { // set arc in real world coords and transform to screen coords drawArc.setArcByCenter(m[0], m[1], r, -as, -ae, GArc2D.PIE); shape = view.getCoordTransform().createTransformedShape(drawArc); double rdiff; // For Decoration switch (geo.getDecorationType()) { default: // do nothing break; case GeoElement.DECORATION_ANGLE_TWO_ARCS: rdiff = 4 + geo.getLineThickness() / 2d; r = (arcSize - rdiff) * view.getInvXscale(); decoArc.setArcByCenter(m[0], m[1], r, -as, -ae, GArc2D.OPEN); // transform arc to screen coords shapeArc1 = view.getCoordTransform() .createTransformedShape(decoArc); break; case GeoElement.DECORATION_ANGLE_THREE_ARCS: rdiff = 4 + geo.getLineThickness() / 2d; r = (arcSize - rdiff) * view.getInvXscale(); decoArc.setArcByCenter(m[0], m[1], r, -as, -ae, GArc2D.OPEN); // transform arc to screen coords shapeArc1 = view.getCoordTransform() .createTransformedShape(decoArc); r = (arcSize - 2 * rdiff) * view.getInvXscale(); decoArc.setArcByCenter(m[0], m[1], r, -as, -ae, GArc2D.OPEN); // transform arc to screen coords shapeArc2 = view.getCoordTransform() .createTransformedShape(decoArc); break; case GeoElement.DECORATION_ANGLE_ONE_TICK: angleTick[0] = -angSt - angExt / 2; updateTick(angleTick[0], arcSize, 0); break; case GeoElement.DECORATION_ANGLE_TWO_TICKS: angleTick[0] = -angSt - 2 * angExt / 5; angleTick[1] = -angSt - 3 * angExt / 5; if (Math.abs(angleTick[1] - angleTick[0]) > MAX_TICK_DISTANCE) { angleTick[0] = -angSt - angExt / 2 - MAX_TICK_DISTANCE / 2; angleTick[1] = -angSt - angExt / 2 + MAX_TICK_DISTANCE / 2; } updateTick(angleTick[0], arcSize, 0); updateTick(angleTick[1], arcSize, 1); break; case GeoElement.DECORATION_ANGLE_THREE_TICKS: angleTick[0] = -angSt - 3 * angExt / 8; angleTick[1] = -angSt - 5 * angExt / 8; if (Math.abs(angleTick[1] - angleTick[0]) > 2 * MAX_TICK_DISTANCE) { angleTick[0] = -angSt - angExt / 2 - MAX_TICK_DISTANCE; angleTick[1] = -angSt - angExt / 2 + MAX_TICK_DISTANCE; } updateTick(angleTick[0], arcSize, 0); updateTick(angleTick[1], arcSize, 1); // middle tick angleTick[0] = -angSt - angExt / 2; updateTick(angleTick[0], arcSize, 2); break; // Michael Borcherds 2007-11-19 START case GeoElement.DECORATION_ANGLE_ARROW_ANTICLOCKWISE: case GeoElement.DECORATION_ANGLE_ARROW_CLOCKWISE: double n2[] = new double[2]; // actual angle for arrow point double n[] = new double[2]; // adjusted to rotate arrow slightly double v[] = new double[2]; // adjusted to rotate arrow slightly double rotateangle = 0.25d; // rotate arrow slightly if (geo.getDecorationType() == GeoElement.DECORATION_ANGLE_ARROW_CLOCKWISE) { n2[0] = Math.cos(angSt); n2[1] = Math.sin(angSt); n[0] = Math.cos(angSt + rotateangle); n[1] = Math.sin(angSt + rotateangle); v[0] = -n[1]; v[1] = n[0]; } else { n2[0] = Math.cos(angExt + angSt); n2[1] = Math.sin(angExt + angSt); n[0] = Math.cos(angExt + angSt - rotateangle); n[1] = Math.sin(angExt + angSt - rotateangle); v[0] = n[1]; v[1] = -n[0]; } double p1[] = new double[2]; double p2[] = new double[2]; double p3[] = new double[2]; rdiff = 4 + geo.getLineThickness() / 2d; r = (arcSize) * view.getInvXscale(); p1[0] = m[0] + r * n2[0]; p1[1] = m[1] + r * n2[1]; // arrow tip double size = 4d + geo.getLineThickness() / 4d; size = size * 0.9d; p2[0] = p1[0] + (1 * n[0] + 3 * v[0]) * size * view.getInvXscale(); p2[1] = p1[1] + (1 * n[1] + 3 * v[1]) * size * view.getInvYscale(); // arrow // end // 1 p3[0] = p1[0] + (-1 * n[0] + 3 * v[0]) * size * view.getInvXscale(); p3[1] = p1[1] + (-1 * n[1] + 3 * v[1]) * size * view.getInvYscale(); // arrow // end // 2 view.toScreenCoords(p1); view.toScreenCoords(p2); view.toScreenCoords(p3); polygon.reset(); polygon.moveTo(p1[0], p1[1]); polygon.lineTo(p2[0], p2[1]); polygon.lineTo(p3[0], p3[1]); polygon.closePath(); break; // Michael Borcherds 2007-11-19 END } // END } // shape on screen? if (!shape.intersects(0, 0, view.getWidth(), view.getHeight())) { setNotVisible(); return; } if (labelVisible) { // calculate label position double radius = r / 1.7; double labelAngle = angSt + angExt / 2.0; coords[0] = m[0] + radius * Math.cos(labelAngle); coords[1] = m[1] + radius * Math.sin(labelAngle); view.toScreenCoords(coords); labelDesc = angle.getLabelDescription(); xLabel = (int) (coords[0] - 3); yLabel = (int) (coords[1] + 5); if (!addLabelOffset() && drawDot) { xLabel = (int) (coords[0] + 2 * geo.getLineThickness()); } } // G.Sturr 2010-6-28 spreadsheet trace is now handled in // GeoElement.update() // if (angle.getSpreadsheetTrace()) // recordToSpreadsheet(angle); } @Override final public void draw(GGraphics2D g2) { if (isVisible) { if (!show90degrees || view .getRightAngleStyle() != EuclidianStyleConstants.RIGHT_ANGLE_STYLE_L) { fill(g2, shape); // fill using default/hatching/image as // appropriate } if (geo.doHighlighting()) { g2.setPaint(angle.getSelColor()); g2.setStroke(selStroke); g2.draw(shape); } if (geo.getLineThickness() > 0) { g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(shape); } // special handling of 90 degree dot if (show90degrees) { switch (view.getRightAngleStyle()) { case EuclidianStyleConstants.RIGHT_ANGLE_STYLE_DOT: g2.fill(dot90degree); break; default: // nothing to do as square for // EuclidianView.RIGHT_ANGLE_STYLE_SQUARE // was already drawn as shape } } else { // if we don't have a special 90 degrees appearance we might // need to draw // other decorations switch (geo.getDecorationType()) { case GeoElement.DECORATION_ANGLE_TWO_ARCS: g2.draw(shapeArc1); break; case GeoElement.DECORATION_ANGLE_THREE_ARCS: g2.draw(shapeArc1); g2.draw(shapeArc2); break; case GeoElement.DECORATION_ANGLE_ONE_TICK: g2.setStroke(decoStroke); g2.draw(tick[0]); break; case GeoElement.DECORATION_ANGLE_TWO_TICKS: g2.setStroke(decoStroke); g2.draw(tick[0]); g2.draw(tick[1]); break; case GeoElement.DECORATION_ANGLE_THREE_TICKS: g2.setStroke(decoStroke); g2.draw(tick[0]); g2.draw(tick[1]); g2.draw(tick[2]); break; // Michael Borcherds 2007-11-19 START case GeoElement.DECORATION_ANGLE_ARROW_ANTICLOCKWISE: case GeoElement.DECORATION_ANGLE_ARROW_CLOCKWISE: g2.setStroke(decoStroke); g2.fill(polygon); break; // Michael Borcherds 2007-11-19 } } if (labelVisible) { g2.setPaint(angle.getLabelColor()); g2.setFont(view.getFontAngle()); drawLabel(g2); } } } // update coords for the tick decoration // tick is at distance radius and oriented towards angle // id = 0,1, or 2 for tick[0],tick[1] or tick[2] private void updateTick(double angle1, int radius, int id) { // coords have to be set to screen coords of m before calling this // method if (tick == null) { tick = new GLine2D[3]; for (int i = 0; i < tick.length; i++) { tick[i] = AwtFactory.getPrototype().newLine2D(); } } double cos = Math.cos(angle1); double sin = Math.sin(angle1); double length = 2.5 + geo.getLineThickness() / 4d; tick[id].setLine(coords[0] + (radius - length) * cos, coords[1] + (radius - length) * sin * view.getScaleRatio(), coords[0] + (radius + length) * cos, coords[1] + (radius + length) * sin * view.getScaleRatio()); } @Override final public boolean hit(int x, int y, int hitThreshold) { return shape != null && shape.contains(x, y); } @Override final public boolean isInside(GRectangle rect) { return shape != null && rect.contains(shape.getBounds()); } @Override public boolean intersectsRectangle(GRectangle rect) { return shape != null && shape.intersects(rect); } @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() || shape == null || !geo.isEuclidianVisible()) { return null; } // return selection circle's bounding box return shape.getBounds(); } private void initPreview() { // init the conic for preview Construction cons = previewTempPoints[0].getConstruction(); AlgoAnglePoints algoPreview = new AlgoAnglePoints(cons, previewTempPoints[0], previewTempPoints[1], previewTempPoints[2]); cons.removeFromConstructionList(algoPreview); geo = algoPreview.getAngle(); angle = (GeoAngle) geo; geo.setEuclidianVisible(true); init(); // initConic(algo.getCircle()); } @Override final public void updatePreview() { if (geo == null || prevPoints.size() != 2) { setNotVisible(); return; } for (int i = 0; i < prevPoints.size(); i++) { Coords p = view .getCoordsForView(prevPoints.get(i).getInhomCoordsInD3()); previewTempPoints[i].setCoords(p, true); } previewTempPoints[0].updateCascade(); } @Override final public void updateMousePos(double xRW, double yRW) { if (isVisible) { previewTempPoints[previewTempPoints.length - 1].setCoords(xRW, yRW, 1.0); previewTempPoints[previewTempPoints.length - 1].updateCascade(); update(); } } @Override final public void drawPreview(GGraphics2D g2) { isVisible = geo != null && prevPoints.size() == 2; // shape may be null if the second point is placed and mouse did not yet // move away from it if (shape != null) { draw(g2); } } @Override public void disposePreview() { // do nothing } /** * @param vertexScreen * RW vertex coordinates */ public void toScreenCoords(double[] vertexScreen) { view.toScreenCoords(vertexScreen); } /** * @param d * maximal radius */ public void setMaxRadius(double d) { this.maxRadius = d; } @Override public BoundingBox getBoundingBox() { // TODO Auto-generated method stub return null; } @Override public void updateBoundingBox() { // TODO Auto-generated method stub } }