/* 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. */ /* * DrawLine.java * * Created on 11. Oktober 2001, 23:59 */ package org.geogebra.common.euclidian.draw; import java.util.ArrayList; import org.geogebra.common.awt.GArea; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.awt.GLine2D; import org.geogebra.common.awt.GPoint2D; import org.geogebra.common.awt.GRectangle; import org.geogebra.common.euclidian.BoundingBox; import org.geogebra.common.euclidian.Drawable; import org.geogebra.common.euclidian.EuclidianStatic; import org.geogebra.common.euclidian.EuclidianView; import org.geogebra.common.euclidian.GeneralPathClipped; import org.geogebra.common.euclidian.Previewable; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.ConstructionDefaults; import org.geogebra.common.kernel.Matrix.CoordMatrix; import org.geogebra.common.kernel.Matrix.Coords; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoLine; import org.geogebra.common.kernel.geos.GeoPoint; import org.geogebra.common.kernel.geos.GeoVec3D; import org.geogebra.common.kernel.kernelND.GeoLineND; import org.geogebra.common.kernel.kernelND.GeoPointND; import org.geogebra.common.util.MyMath; /** * Draws a line or a ray. */ public class DrawLine extends Drawable implements Previewable { // clipping attributes private static final int LEFT = 0; private static final int RIGHT = 1; private static final int TOP = 2; private static final int BOTTOM = 3; /** preview types */ public enum PreviewType { /** none */ NONE, /** line through points */ LINE, /** parallel line */ PARALLEL, /** perpendicular line */ PERPENDICULAR, /** perpendicular bisector */ PERPENDICULAR_BISECTOR, /** angle bisector */ ANGLE_BISECTOR } private GeoLineND g; // private double [] coeffs = new double[3]; private GLine2D line; /** y-coord of first endpoint */ public double y1; /** y-coord of second endpoint */ public double y2; /** x-coord of first endpoint */ public double x1; /** x-coord of second endpoint */ public double x2; private double k; private double d; private double gx; private double gy; private double gz; private int labelPos = LEFT, p1Pos, p2Pos; private int x, y; private boolean isVisible; private boolean labelVisible; private ArrayList<GeoPointND> points;// for preview private ArrayList<GeoLineND> lines; // for preview private GeoPointND startPoint, previewPoint2; // clipping attributes private boolean[] attr1 = new boolean[4], attr2 = new boolean[4]; /** * Creates new DrawLine * * @param view * view * @param g * line */ public DrawLine(EuclidianView view, GeoLineND g) { this.view = view; this.g = g; geo = (GeoElement) g; update(); } /** * Creates a new DrawLine for preview. * * @param view * view * @param points * preview points * @param previewMode * preview mode */ public DrawLine(EuclidianView view, ArrayList<GeoPointND> points, PreviewType previewMode) { this.previewMode = previewMode; this.view = view; this.points = points; g = new GeoLine(view.getKernel().getConstruction()); geo = view.getKernel().getConstruction().getConstructionDefaults() .getDefaultGeo(ConstructionDefaults.DEFAULT_LINE); updatePreview(); } private PreviewType previewMode = PreviewType.NONE; /** * Creates a new DrawLine for preview of parallel or perpendicular tool * * @param view * view * @param points * preview points * @param lines * preview lines * @param parallel * true for paralel, false for perpendicular */ public DrawLine(EuclidianView view, ArrayList<GeoPointND> points, ArrayList<GeoLineND> lines, boolean parallel) { if (parallel) { previewMode = PreviewType.PARALLEL; } else { previewMode = PreviewType.PERPENDICULAR; } this.view = view; this.points = points; this.lines = lines; g = new GeoLine(view.getKernel().getConstruction()); geo = view.getKernel().getConstruction().getConstructionDefaults() .getDefaultGeo(ConstructionDefaults.DEFAULT_LINE); updatePreview(); } @Override public void update() { update(view.getMatrix()); } /** * update the drawable with a view matrix * * @param matrix * view matrix */ public void update(CoordMatrix matrix) { // take line g here, not geo this object may be used for conics too isVisible = geo.isEuclidianVisible(); if (isVisible) { labelVisible = geo.isLabelVisible(); updateStrokes(g); Coords equation = g.getCartesianEquationVector(matrix); if (equation == null || !equation.isFinite()) { isVisible = false; return; } gx = equation.getX(); gy = equation.getY(); gz = equation.getZ(); setClippedLine(); // line on screen? if (!line.intersects(-EuclidianStatic.CLIP_DISTANCE, -EuclidianStatic.CLIP_DISTANCE, view.getWidth() + EuclidianStatic.CLIP_DISTANCE, view.getHeight() + EuclidianStatic.CLIP_DISTANCE)) { isVisible = false; // don't return here to make sure that getBounds() works for // offscreen points too } // draw trace if (g.getTrace()) { isTracing = true; GGraphics2D g2 = view.getBackgroundGraphics(); if (g2 != null) { drawTrace(g2); } } else { if (isTracing) { isTracing = false; // view.updateBackground(); } } if (labelVisible) { labelDesc = geo.getLabelDescription(); setLabelPosition(); addLabelOffsetEnsureOnScreen(view.getFontLine()); } } } // transform line to screen coords // write start and endpoint into (x1,y1), (x2,y2) private final void setClippedLine() { // first calc two points in screen coords that are on the line // abs(slope) < 1 // y = k x + d // x1 = 0, x2 = width if (Math.abs(gx) * view.getScaleRatio() < Math.abs(gy)) { // calc points on line in screen coords k = gx / gy * view.getScaleRatio(); d = view.getYZero() + gz / gy * view.getYscale() - k * view.getXZero(); x1 = -EuclidianStatic.CLIP_DISTANCE; y1 = k * x1 + d; x2 = view.getWidth() + EuclidianStatic.CLIP_DISTANCE; y2 = k * x2 + d; p1Pos = LEFT; p2Pos = RIGHT; clipTopBottom(); } // abs(slope) >= 1 // x = k y + d // y1 = height, y2 = 0 else { // calc points on line in screen coords k = gy / (gx * view.getScaleRatio()); d = view.getXZero() - gz / gx * view.getXscale() - k * view.getYZero(); y1 = view.getHeight() + EuclidianStatic.CLIP_DISTANCE; x1 = k * y1 + d; y2 = -EuclidianStatic.CLIP_DISTANCE; x2 = k * y2 + d; p1Pos = BOTTOM; p2Pos = TOP; clipLeftRight(); } if (line == null) { line = AwtFactory.getPrototype().newLine2D(); } line.setLine(x1, y1, x2, y2); } // Cohen & Sutherland algorithm for line clipping on a rectangle // Computergraphics I (Prof. Held) pp.100 // points (0, y1), (width, y2) -> clip on y=0 and y=height final private void clipTopBottom() { // calc clip attributes for both points (x1,y1), (x2,y2) attr1[TOP] = y1 < -EuclidianStatic.CLIP_DISTANCE; attr1[BOTTOM] = y1 > view.getHeight() + EuclidianStatic.CLIP_DISTANCE; attr2[TOP] = y2 < -EuclidianStatic.CLIP_DISTANCE; attr2[BOTTOM] = y2 > view.getHeight() + EuclidianStatic.CLIP_DISTANCE; // both points outside (TOP or BOTTOM) if ((attr1[TOP] && attr2[TOP]) || (attr1[BOTTOM] && attr2[BOTTOM])) { return; } // at least one point inside -> clip // point1 TOP -> clip with y=0 if (attr1[TOP]) { y1 = -EuclidianStatic.CLIP_DISTANCE; x1 = (y1 - d) / k; p1Pos = TOP; } // point1 BOTTOM -> clip with y=height else if (attr1[BOTTOM]) { y1 = view.getHeight() + EuclidianStatic.CLIP_DISTANCE; x1 = (y1 - d) / k; p1Pos = BOTTOM; } // point2 TOP -> clip with y=0 if (attr2[TOP]) { y2 = -EuclidianStatic.CLIP_DISTANCE; x2 = (y2 - d) / k; p2Pos = TOP; } // point2 BOTTOM -> clip with y=height else if (attr2[BOTTOM]) { y2 = view.getHeight() + EuclidianStatic.CLIP_DISTANCE; x2 = (y2 - d) / k; p2Pos = BOTTOM; } } // Cohen & Sutherland algorithm for line clipping on a rectangle // Computergraphics I (Prof. Held) pp.100 // points (x1, 0), (x2, height) -> clip on x=0 and x=width final private void clipLeftRight() { // calc clip attributes for both points (x1,y1), (x2,y2) attr1[LEFT] = x1 < -EuclidianStatic.CLIP_DISTANCE; attr1[RIGHT] = x1 > view.getWidth() + EuclidianStatic.CLIP_DISTANCE; attr2[LEFT] = x2 < -EuclidianStatic.CLIP_DISTANCE; attr2[RIGHT] = x2 > view.getWidth() + EuclidianStatic.CLIP_DISTANCE; // both points outside (LEFT or RIGHT) if ((attr1[LEFT] && attr2[LEFT]) || (attr1[RIGHT] && attr2[RIGHT])) { return; } // at least one point inside -> clip // point1 LEFT -> clip with x=0 if (attr1[LEFT]) { x1 = -EuclidianStatic.CLIP_DISTANCE; y1 = (x1 - d) / k; p1Pos = LEFT; } // point1 RIGHT -> clip with x=width else if (attr1[RIGHT]) { x1 = view.getWidth() + EuclidianStatic.CLIP_DISTANCE; y1 = (x1 - d) / k; p1Pos = RIGHT; } // point2 LEFT -> clip with x=0 if (attr2[LEFT]) { x2 = -EuclidianStatic.CLIP_DISTANCE; y2 = (x2 - d) / k; p2Pos = LEFT; } // point2 RIGHT -> clip with x=width else if (attr2[RIGHT]) { x2 = view.getWidth() + EuclidianStatic.CLIP_DISTANCE; y2 = (x2 - d) / k; p2Pos = RIGHT; } } // set label position (xLabel, yLabel) private final void setLabelPosition() { // choose smallest position change // 1-Norm distance between old label position // and point 1, point 2 if (Math.abs(xLabel - x1) + Math.abs(yLabel - y1) > Math.abs(xLabel - x2) + Math.abs(yLabel - y2)) { x = (int) x2; y = (int) y2; labelPos = p2Pos; } else { x = (int) x1; y = (int) y1; labelPos = p1Pos; } // constant to respect slope of line for additional space // slope for LEFT, RIGHT: k = gx/gy // slope for TOP, BOTTOM: 1/k = gy/gx switch (labelPos) { case LEFT: xLabel = 5; if (2 * y < view.getHeight()) { yLabel = y + 16 + (int) (16 * (gx / gy)); } else { yLabel = y - 8 + (int) (16 * (gx / gy)); } break; case RIGHT: xLabel = view.getWidth() - 15; if (2 * y < view.getHeight()) { yLabel = y + 16 - (int) (16 * (gx / gy)); } else { yLabel = y - 8 - (int) (16 * (gx / gy)); } break; case TOP: yLabel = 15; if (2 * x < view.getWidth()) { xLabel = x + 8 + (int) (16 * (gy / gx)); } else { xLabel = x - 16 + (int) (16 * (gy / gx)); } break; case BOTTOM: yLabel = view.getHeight() - 5; if (2 * x < view.getWidth()) { xLabel = x + 8 - (int) (16 * (gy / gx)); } else { xLabel = x - 16 - (int) (16 * (gy / gx)); } break; } } @Override public void draw(GGraphics2D g2) { if (isVisible) { if (geo.doHighlighting()) { // draw line g2.setPaint(geo.getSelColor()); g2.setStroke(selStroke); g2.draw(line); } // draw line g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(line); // label if (labelVisible) { g2.setFont(view.getFontLine()); g2.setColor(geo.getLabelColor()); drawLabel(g2); } } } /** * Draws a horizontal segment just used for preview line style * * @param g2 * Graphics to draw. * @param marginX * Space from left and right. * @param marginY * Space from top and bottom. * @param width * The width of the segment. * */ public void drawStylePreview(GGraphics2D g2, int marginX, int marginY, int width) { updateStrokes(geo); g2.setStroke(objStroke); g2.setColor(geo.getObjectColor()); g2.drawStraightLine(marginX, marginY, width - marginX, marginY); } /** * For updating stylebar position * * @return 0 sized rectangle with top-left corner close to the center of the * visible part of the line */ public GRectangle getPreferredStylebarPosition() { GRectangle rect = AwtFactory.getPrototype().newRectangle(0, 0); rect.setBounds((int) (x1 + x2) / 2 + 50, (int) (y1 + y2) / 2 + 50, 0, 0); return rect; } @Override public final void drawTrace(GGraphics2D g2) { g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(line); } @Override final public void updatePreview() { switch (previewMode) { default: case LINE: case PERPENDICULAR_BISECTOR: isVisible = (points.size() == 1); if (isVisible) { startPoint = points.get(0); } break; case PARALLEL: case PERPENDICULAR: isVisible = (lines.size() == 1); break; case ANGLE_BISECTOR: isVisible = (points.size() == 2); if (isVisible) { startPoint = points.get(0); previewPoint2 = points.get(1); } break; } } private GPoint2D endPoint = AwtFactory.getPrototype().newPoint2D(); private final Coords coordsForMousePos = new Coords(4); @Override public void updateMousePos(double mouseRWx, double mouseRWy) { double xRW = mouseRWx; double yRW = mouseRWy; isPreviewVisible = false; if (isVisible) { switch (previewMode) { default: case LINE: // round angle to nearest 15 degrees if alt pressed if (points.size() == 1 && view.getEuclidianController().isAltDown()) { GeoPoint p = (GeoPoint) points.get(0); double px = p.inhomX; double py = p.inhomY; double angle = Math.atan2(yRW - py, xRW - px) * 180 / Math.PI; double radius = Math.sqrt( (py - yRW) * (py - yRW) + (px - xRW) * (px - xRW)); // round angle to nearest 15 degrees angle = Math.round(angle / 15) * 15; xRW = px + radius * Math.cos(angle * Math.PI / 180); yRW = py + radius * Math.sin(angle * Math.PI / 180); endPoint.setX(xRW); endPoint.setY(yRW); view.getEuclidianController().setLineEndPoint(endPoint); } else { view.getEuclidianController().setLineEndPoint(null); } // line through first point and mouse position // coords = startPoint.getCoordsInD2().crossProduct(new // Coords(xRW, yRW, 1)); this.coordsForMousePos.setCrossProduct( view.getCoordsForView(startPoint.getInhomCoordsInD3()) .projectInfDim(), new Coords(xRW, yRW, 1)); ((GeoLine) g).setCoords(coordsForMousePos.getX(), coordsForMousePos.getY(), coordsForMousePos.getZ()); // GeoVec3D.cross(startPoint, xRW, yRW, 1.0, g); break; case PARALLEL: // calc the line g through (xRW,yRW) and parallel to l GeoLineND lND = lines.get(0); Coords equation = lND .getCartesianEquationVector(view.getMatrix()); GeoVec3D.cross(xRW, yRW, 1.0, equation.getY(), -equation.getX(), 0.0, ((GeoLine) g)); break; case PERPENDICULAR: // calc the line g through (xRW,yRW) and perpendicular to l lND = lines.get(0); equation = lND.getCartesianEquationVector(view.getMatrix()); GeoVec3D.cross(xRW, yRW, 1.0, equation.getX(), equation.getY(), 0.0, ((GeoLine) g)); break; case PERPENDICULAR_BISECTOR: // calc the perpendicular bisector coordsForMousePos.set(startPoint.getInhomCoordsInD2()); double startx = coordsForMousePos.getX(); double starty = coordsForMousePos.getY(); GeoVec3D.cross((xRW + startx) / 2, (yRW + starty) / 2, 1.0, -yRW + starty, xRW - startx, 0.0, ((GeoLine) g)); break; case ANGLE_BISECTOR: GeoLine g1 = new GeoLine(view.getKernel().getConstruction()); GeoLine h = new GeoLine(view.getKernel().getConstruction()); // GeoVec3D.cross(previewPoint2, startPoint, g1); // GeoVec3D.cross(previewPoint2, xRW, yRW, 1.0, h); coordsForMousePos.setCrossProduct(previewPoint2.getCoordsInD2(), startPoint.getCoordsInD2()); g1.setCoords(coordsForMousePos.getX(), coordsForMousePos.getY(), coordsForMousePos.getZ()); coordsForMousePos.setCrossProduct(previewPoint2.getCoordsInD2(), new Coords(xRW, yRW, 1)); h.setCoords(coordsForMousePos.getX(), coordsForMousePos.getY(), coordsForMousePos.getZ()); // (gx, gy) is direction of g = B v A double g2x = g1.y; double g2y = -g1.x; double lenG = MyMath.length(g2x, g2y); g2x /= lenG; g2y /= lenG; // (hx, hy) is direction of h = B v C double hx = h.y; double hy = -h.x; double lenH = MyMath.length(hx, hy); hx /= lenH; hy /= lenH; // set direction vector of bisector: (wx, wy) double wx, wy; // calc direction vector (wx, wy) of angular bisector // check if angle between vectors is > 90 degrees double ip = g2x * hx + g2y * hy; if (ip >= 0.0) { // angle < 90 degrees // standard case wx = g2x + hx; wy = g2y + hy; } else { // ip <= 0.0, angle > 90 degrees // BC - BA is a normalvector of the bisector wx = hy - g2y; wy = g2x - hx; // if angle > 180 degree change orientation of direction // det(g,h) < 0 if (g2x * hy < g2y * hx) { wx = -wx; wy = -wy; } } // make (wx, wy) a unit vector double length = MyMath.length(wx, wy); wx /= length; wy /= length; // wv.x = wx; // wv.y = wy; // set bisector this.coordsForMousePos.set(previewPoint2.getInhomCoordsInD2()); ((GeoLine) g).x = -wy; ((GeoLine) g).y = wx; ((GeoLine) g).z = -(coordsForMousePos.getX() * ((GeoLine) g).x + coordsForMousePos.getY() * ((GeoLine) g).y); break; } if (((GeoLine) g).isZero()) { isVisible = false; return; } isPreviewVisible = true; gx = ((GeoLine) g).x; gy = ((GeoLine) g).y; gz = ((GeoLine) g).z; setClippedLine(); } } private boolean isPreviewVisible; @Override final public void drawPreview(GGraphics2D g2) { if (isPreviewVisible) { g2.setPaint(getObjectColor()); updateStrokes(geo); g2.setStroke(objStroke); g2.draw(line); } } @Override public void disposePreview() { // do nothing } /** * was this object clicked at? (mouse pointer location (x,y) in screen * coords) */ @Override final public boolean hit(int screenx, int screeny, int hitThreshold) { return isVisible && line.intersects(screenx - hitThreshold, screeny - hitThreshold, 2 * hitThreshold, 2 * hitThreshold); } @Override final public boolean isInside(GRectangle rect) { return false; } @Override public boolean intersectsRectangle(GRectangle rect) { return line.intersects(rect); } @Override final public GeoElement getGeoElement() { return geo; } @Override final public void setGeoElement(GeoElement geo) { this.geo = geo; } @Override public GArea getShape() { return getShape(false); } /** * @param forConic * when true, we select the part containing top right screen * corner. otherwise we pick the one above the line. * @return one halfplane wrt this line */ public GArea getShape(boolean forConic) { GeneralPathClipped gpc = new GeneralPathClipped(view); boolean invert = g.isInverseFill(); if (x1 > x2) { double swap = x1; x1 = x2; x2 = swap; swap = y1; y1 = y2; y2 = swap; } gpc.moveTo(x1, y1); gpc.lineTo(x2, y2); // cross top and bottom if (x1 > 0 && x2 <= view.getWidth()) { if (y2 < y1) { gpc.lineTo(0, 0); gpc.lineTo(0, view.getHeight()); } else { gpc.lineTo(0, view.getHeight()); gpc.lineTo(0, 0); if (!forConic) { invert = !invert; } } } // cross top/bottom and right else if (x1 > 0 && x2 > view.getWidth()) { gpc.lineTo(view.getWidth(), y1); invert ^= forConic || (y1 > 0); } // cros left and bottom/top else if (x1 <= 0 && x2 <= view.getWidth()) { gpc.lineTo(0, y2); invert ^= y2 > 0; } // cross left and right else { gpc.lineTo(view.getWidth(), 0); gpc.lineTo(0, 0); } gpc.closePath(); GArea gpcArea = AwtFactory.getPrototype().newArea(gpc); if (!invert) { return gpcArea; } GArea complement = AwtFactory.getPrototype() .newArea(view.getBoundingPath()); complement.subtract(gpcArea); return complement; } /** * Returns the bounding box of this Drawable in screen coordinates. */ @Override final public GRectangle getBounds() { if (line == null || !geo.isDefined() || !geo.isEuclidianVisible()) { return null; } return AwtFactory.getPrototype().newRectangle(line.getBounds()); } @Override public BoundingBox getBoundingBox() { // TODO Auto-generated method stub return null; } @Override public void updateBoundingBox() { // TODO Auto-generated method stub } }