/* 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. */ /* * DrawSegment * * Created on 21. 8 . 2003 */ 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.euclidian.BoundingBox; import org.geogebra.common.euclidian.Drawable; import org.geogebra.common.euclidian.EuclidianBoundingBoxHandler; import org.geogebra.common.euclidian.EuclidianStatic; import org.geogebra.common.euclidian.EuclidianView; import org.geogebra.common.euclidian.Previewable; import org.geogebra.common.euclidian.clipping.ClipLine; import org.geogebra.common.euclidian.event.AbstractEvent; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.ConstructionDefaults; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.Matrix.Coords; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoPoint; import org.geogebra.common.kernel.kernelND.GeoLineND; import org.geogebra.common.kernel.kernelND.GeoPointND; import org.geogebra.common.util.MyMath; /** * * @author Markus Hohenwarter */ public class DrawSegment extends Drawable implements Previewable { private GeoLineND s; private boolean isVisible, labelVisible; private ArrayList<GeoPointND> points; private GLine2D line; private double[] coordsA = new double[2]; private double[] coordsB = new double[2]; // For drawing ticks private GLine2D[] decoTicks; private BoundingBox boundingBox; /** * Creates new DrawSegment * * @param view * Euclidian view to be used * @param s * Segment to be drawn */ public DrawSegment(EuclidianView view, GeoLineND s) { this.view = view; this.s = s; geo = (GeoElement) s; update(); } /** * Creates a new DrawSegment for preview. * * @param view * Euclidian view to be used * @param points * endpoints of the segment */ public DrawSegment(EuclidianView view, ArrayList<GeoPointND> points) { this.view = view; this.points = points; geo = view.getKernel().getConstruction().getConstructionDefaults() .getDefaultGeo(ConstructionDefaults.DEFAULT_SEGMENT); updatePreview(); } @Override final public void update() { isVisible = geo.isEuclidianVisible(); if (!isVisible) { return; } Coords A = view.getCoordsForView(s.getStartInhomCoords()); // check if in view if (!Kernel.isZero(A.getZ()) || !A.isFinite()) { isVisible = false; return; } Coords B = view.getCoordsForView(s.getEndInhomCoords()); // check if in view if (!Kernel.isZero(B.getZ()) || !B.isFinite()) { isVisible = false; return; } update(A, B); if (geo.isShape()) { if (getBounds() != null) { getBoundingBox().setRectangle(getBounds()); // for segment only two handler boundingBox.getHandlers().get(0).setFrameFromCenter( line.getX1(), line.getY1(), line.getX1() + 5, line.getY1() + 5); boundingBox.getHandlers().get(1).setFrameFromCenter( line.getX2(), line.getY2(), line.getX2() + 5, line.getY2() + 5); // handler for rotation // boundingBox.getHandlers().get(2).setFrameFromCenter( // (getBounds().getMinX() + getBounds().getMaxX()) / 2, // getBounds().getMaxY() + 15, // (getBounds().getMinX() + getBounds().getMaxX()) / 2 + 3, // getBounds().getMaxY() + 15 + 3); } else { getBoundingBox().setRectangle(null); } } } /** * update with A, B for end points * * @param A * end point * @param B * end point */ final public void update(Coords A, Coords B) { labelVisible = geo.isLabelVisible(); updateStrokes(geo); coordsA[0] = A.getX(); coordsA[1] = A.getY(); coordsB[0] = B.getX(); coordsB[1] = B.getY(); boolean onscreenA = view.toScreenCoords(coordsA); boolean onscreenB = view.toScreenCoords(coordsB); if (line == null) { line = AwtFactory.getPrototype().newLine2D(); } if (onscreenA && onscreenB) { // A and B on screen line.setLine(coordsA[0], coordsA[1], coordsB[0], coordsB[1]); } else { // A or B off screen // clip at screen, that's important for huge coordinates isVisible = drawClipped(coordsA, coordsB, line, -EuclidianStatic.CLIP_DISTANCE, view.getWidth() + EuclidianStatic.CLIP_DISTANCE, -EuclidianStatic.CLIP_DISTANCE, view.getHeight() + EuclidianStatic.CLIP_DISTANCE); } // draw trace if (s.getTrace()) { isTracing = true; GGraphics2D g2 = view.getBackgroundGraphics(); if (g2 != null) { drawTrace(g2); } } else { if (isTracing) { isTracing = false; // view.updateBackground(); } } // if no label and no decoration then we're done if (!labelVisible && geo.getDecorationType() == GeoElement.DECORATION_NONE) { return; } // calc midpoint (midX, midY) and perpendicular vector (nx, ny) double midX = (coordsA[0] + coordsB[0]) / 2.0; double midY = (coordsA[1] + coordsB[1]) / 2.0; double nx = coordsA[1] - coordsB[1]; double ny = coordsB[0] - coordsA[0]; double nLength = MyMath.length(nx, ny); // label position // use unit perpendicular vector to move away from line if (labelVisible) { labelDesc = geo.getLabelDescription(); if (nLength > 0.0) { xLabel = (int) (midX + nx * 16 / nLength); yLabel = (int) (midY + ny * 16 / nLength); } else { xLabel = (int) midX; yLabel = (int) (midY + 16); } addLabelOffset(); } // update decoration if (geo.getDecorationType() != GeoElement.DECORATION_NONE && nLength > 0) { if (decoTicks == null) { // only create these object when they are really needed decoTicks = new GLine2D[6]; // Michael Borcherds 20071006 // changed from 3 to 6 for (int i = 0; i < decoTicks.length; i++) { decoTicks[i] = AwtFactory.getPrototype().newLine2D(); } } // tick spacing and length. double tickSpacing = 2.5 + geo.getLineThickness() / 2d; double tickLength = tickSpacing + 1; // Michael Borcherds 20071006 start double arrowlength = 1.5; // Michael Borcherds 20071006 end double vx, vy, factor; switch (geo.getDecorationType()) { default: // do nothing break; case GeoElement.DECORATION_SEGMENT_ONE_TICK: // use perpendicular vector to set tick factor = tickLength / nLength; nx *= factor; ny *= factor; decoTicks[0].setLine(midX - nx, midY - ny, midX + nx, midY + ny); break; case GeoElement.DECORATION_SEGMENT_TWO_TICKS: // vector (vx, vy) to get 2 points around midpoint factor = tickSpacing / (2 * nLength); vx = -ny * factor; vy = nx * factor; // use perpendicular vector to set ticks factor = tickLength / nLength; nx *= factor; ny *= factor; decoTicks[0].setLine(midX + vx - nx, midY + vy - ny, midX + vx + nx, midY + vy + ny); decoTicks[1].setLine(midX - vx - nx, midY - vy - ny, midX - vx + nx, midY - vy + ny); break; case GeoElement.DECORATION_SEGMENT_THREE_TICKS: // vector (vx, vy) to get 2 points around midpoint factor = tickSpacing / nLength; vx = -ny * factor; vy = nx * factor; // use perpendicular vector to set ticks factor = tickLength / nLength; nx *= factor; ny *= factor; decoTicks[0].setLine(midX + vx - nx, midY + vy - ny, midX + vx + nx, midY + vy + ny); decoTicks[1].setLine(midX - nx, midY - ny, midX + nx, midY + ny); decoTicks[2].setLine(midX - vx - nx, midY - vy - ny, midX - vx + nx, midY - vy + ny); break; case GeoElement.DECORATION_SEGMENT_ONE_ARROW: // vector (vx, vy) to get 2 points around midpoint factor = tickSpacing / (1.5 * nLength); vx = -ny * factor; vy = nx * factor; // use perpendicular vector to set tick factor = tickLength / (1.5 * nLength); nx *= factor; ny *= factor; decoTicks[0].setLine(midX - arrowlength * vx, midY - arrowlength * vy, midX - arrowlength * vx + arrowlength * (nx + vx), midY - arrowlength * vy + arrowlength * (ny + vy)); decoTicks[1].setLine(midX - arrowlength * vx, midY - arrowlength * vy, midX - arrowlength * vx + arrowlength * (-nx + vx), midY - arrowlength * vy + arrowlength * (-ny + vy)); break; case GeoElement.DECORATION_SEGMENT_TWO_ARROWS: // vector (vx, vy) to get 2 points around midpoint factor = tickSpacing / (1.5 * nLength); vx = -ny * factor; vy = nx * factor; // use perpendicular vector to set ticks factor = tickLength / (1.5 * nLength); nx *= factor; ny *= factor; decoTicks[0].setLine(midX - 2 * arrowlength * vx, midY - 2 * arrowlength * vy, midX - 2 * arrowlength * vx + arrowlength * (nx + vx), midY - 2 * arrowlength * vy + arrowlength * (ny + vy)); decoTicks[1].setLine(midX - 2 * arrowlength * vx, midY - 2 * arrowlength * vy, midX - 2 * arrowlength * vx + arrowlength * (-nx + vx), midY - 2 * arrowlength * vy + arrowlength * (-ny + vy)); decoTicks[2].setLine(midX, midY, midX + arrowlength * (nx + vx), midY + arrowlength * (ny + vy)); decoTicks[3].setLine(midX, midY, midX + arrowlength * (-nx + vx), midY + arrowlength * (-ny + vy)); break; case GeoElement.DECORATION_SEGMENT_THREE_ARROWS: // vector (vx, vy) to get 2 points around midpoint factor = tickSpacing / (1.5 * nLength); vx = -ny * factor; vy = nx * factor; // use perpendicular vector to set ticks factor = tickLength / (1.5 * nLength); nx *= factor; ny *= factor; decoTicks[0].setLine(midX - arrowlength * vx, midY - arrowlength * vy, midX - arrowlength * vx + arrowlength * (nx + vx), midY - arrowlength * vy + arrowlength * (ny + vy)); decoTicks[1].setLine(midX - arrowlength * vx, midY - arrowlength * vy, midX - arrowlength * vx + arrowlength * (-nx + vx), midY - arrowlength * vy + arrowlength * (-ny + vy)); decoTicks[2].setLine(midX + arrowlength * vx, midY + arrowlength * vy, midX + arrowlength * vx + arrowlength * (nx + vx), midY + arrowlength * vy + arrowlength * (ny + vy)); decoTicks[3].setLine(midX + arrowlength * vx, midY + arrowlength * vy, midX + arrowlength * vx + arrowlength * (-nx + vx), midY + arrowlength * vy + arrowlength * (-ny + vy)); decoTicks[4].setLine(midX - 3 * arrowlength * vx, midY - 3 * arrowlength * vy, midX - 3 * arrowlength * vx + arrowlength * (nx + vx), midY - 3 * arrowlength * vy + arrowlength * (ny + vy)); decoTicks[5].setLine(midX - 3 * arrowlength * vx, midY - 3 * arrowlength * vy, midX - 3 * arrowlength * vx + arrowlength * (-nx + vx), midY - 3 * arrowlength * vy + arrowlength * (-ny + vy)); break; } } else { // #4907 make sure decorations disappear for length 0 segments if (decoTicks != null) { for (int i = 0; i < decoTicks.length; i++) { decoTicks[i].setLine(Double.NaN, Double.NaN, Double.NaN, Double.NaN); } } } } /** * @param coordsA * first point * @param coordsB * second point * @param line * line to be updated * @param xmin * clip left border * @param xmax * clip right border * @param ymin * clip top * @param ymax * clip bottom * @return whether line intersects clipping rectangle */ public static boolean drawClipped(double[] coordsA, double[] coordsB, GLine2D line, int xmin, int xmax, int ymin, int ymax) { GPoint2D[] clippedPoints = ClipLine.getClipped(coordsA[0], coordsA[1], coordsB[0], coordsB[1], xmin, xmax, ymin, ymax); if (clippedPoints == null) { return false; } line.setLine(clippedPoints[0].getX(), clippedPoints[0].getY(), clippedPoints[1].getX(), clippedPoints[1].getY()); return true; } @Override final public void draw(GGraphics2D g2) { // segments of polygons can have zero thickness if (geo.getLineThickness() == 0) { return; } if (isVisible) { if (geo.doHighlighting()) { g2.setPaint(geo.getSelColor()); g2.setStroke(selStroke); g2.draw(line); } g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(line); // decoTicks is null for zero length segments if (geo.getDecorationType() != GeoElement.DECORATION_NONE && decoTicks != null) { g2.setStroke(decoStroke); switch (geo.getDecorationType()) { default: // do nothing break; case GeoElement.DECORATION_SEGMENT_ONE_TICK: g2.draw(decoTicks[0]); break; case GeoElement.DECORATION_SEGMENT_TWO_TICKS: g2.draw(decoTicks[0]); g2.draw(decoTicks[1]); break; case GeoElement.DECORATION_SEGMENT_THREE_TICKS: g2.draw(decoTicks[0]); g2.draw(decoTicks[1]); g2.draw(decoTicks[2]); break; // Michael Borcherds 20071006 start case GeoElement.DECORATION_SEGMENT_ONE_ARROW: g2.draw(decoTicks[0]); g2.draw(decoTicks[1]); break; case GeoElement.DECORATION_SEGMENT_TWO_ARROWS: g2.draw(decoTicks[0]); g2.draw(decoTicks[1]); g2.draw(decoTicks[2]); g2.draw(decoTicks[3]); break; case GeoElement.DECORATION_SEGMENT_THREE_ARROWS: g2.draw(decoTicks[0]); g2.draw(decoTicks[1]); g2.draw(decoTicks[2]); g2.draw(decoTicks[3]); g2.draw(decoTicks[4]); g2.draw(decoTicks[5]); break; // Michael Borcherds 20071006 end } } // END if (labelVisible) { g2.setPaint(geo.getLabelColor()); g2.setFont(view.getFontLine()); drawLabel(g2); } } } @Override protected final void drawTrace(GGraphics2D g2) { g2.setPaint(getObjectColor()); g2.setStroke(objStroke); g2.draw(line); } @Override final public void updatePreview() { isVisible = points.size() == 1; if (isVisible) { // start point view.getCoordsForView(points.get(0).getInhomCoordsInD3()) .get(coordsA); view.toScreenCoords(coordsA); if (line == null) { line = AwtFactory.getPrototype().newLine2D(); } } } private GPoint2D endPoint = AwtFactory.getPrototype().newPoint2D(); @Override final public void updateMousePos(double mouseRWx, double mouseRWy) { double xRW = mouseRWx; double yRW = mouseRWy; if (isVisible) { // double xRW = view.toRealWorldCoordX(mx); // double yRW = view.toRealWorldCoordY(my); int mx = view.toScreenCoordX(xRW); int my = view.toScreenCoordY(yRW); // 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); mx = view.toScreenCoordX(xRW); my = view.toScreenCoordY(yRW); endPoint.setX(xRW); endPoint.setY(yRW); view.getEuclidianController().setLineEndPoint(endPoint); } else { view.getEuclidianController().setLineEndPoint(null); } line.setLine(coordsA[0], coordsA[1], mx, my); } } @Override final public void drawPreview(GGraphics2D g2) { if (isVisible) { g2.setPaint(getObjectColor()); updateStrokes(geo); g2.setStroke(objStroke); g2.draw(line); } } @Override public void disposePreview() { // do nothing } @Override final public boolean hit(int x, int y, int hitThreshold) { return (line != null && isVisible && line.intersects(x - hitThreshold, y - hitThreshold, 2 * hitThreshold, 2 * hitThreshold)) || (getBoundingBox() != null && getBoundingBox().getRectangle() != null && getBoundingBox() == view.getBoundingBox() && getBoundingBox().getRectangle().intersects( x - hitThreshold, y - hitThreshold, 2 * hitThreshold, 2 * hitThreshold) && getBoundingBox().hitSideOfBoundingBox(x, y, hitThreshold)); } @Override final public boolean isInside(GRectangle rect) { return line != null && rect.contains(line.getP1()) && rect.contains(line.getP2()); } @Override public boolean intersectsRectangle(GRectangle rect) { return line.intersects(rect); } @Override public GeoElement getGeoElement() { return geo; } @Override public void setGeoElement(GeoElement geo) { this.geo = geo; } /** * 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()); } /** * set visible */ public void setIsVisible() { isVisible = true; } /** * update bounding box construction */ @Override public void updateBoundingBox() { if (getBoundingBox().getRectangle() == null) { if (geo.isShape() && getBounds() != null) { boundingBox.setRectangle(getBounds()); // for segment only two handler boundingBox.getHandlers().get(0).setFrameFromCenter( line.getX1(), line.getY1(), line.getX1() + 5, line.getY1() + 5); boundingBox.getHandlers().get(1).setFrameFromCenter( line.getX2(), line.getY2(), line.getX2() + 5, line.getY2() + 5); // handler for rotation // boundingBox.getHandlers().get(2).setFrameFromCenter( // (getBounds().getMinX() + getBounds().getMaxX()) / 2, // getBounds().getMaxY() + 15, // (getBounds().getMinX() + getBounds().getMaxX()) / 2 + 3, // getBounds().getMaxY() + 15 + 3); } } } @Override public BoundingBox getBoundingBox() { if (boundingBox == null) { boundingBox = new BoundingBox(); boundingBox.setNrHandlers(2); } return boundingBox; } @Override public void updateByBoundingBoxResize(AbstractEvent e, EuclidianBoundingBoxHandler handler) { double dP1 = line.getP1().distance(e.getX(), e.getY()); double dP2 = line.getP2().distance(e.getX(), e.getY()); double realX = view.toRealWorldCoordX(e.getX()); double realY = view.toRealWorldCoordY(e.getY()); if (dP1 <= dP2) { s.getStartPoint().setCoords(realX, realY, 1); } else { s.getEndPoint().setCoords(realX, realY, 1); } s.update(); s.updateRepaint(); s.getParentAlgorithm().update(); this.update(); view.getEuclidianController().setDynamicStylebarVisible(false); view.repaintView(); } /** * method to get number of handler for mouse location * * @param mouseLoc * - current mouse location * @return number of handler for mouse location */ public EuclidianBoundingBoxHandler getHandler(GPoint mouseLoc) { if (Kernel.isEqual(getBoundingBox().getRectangle().getMinX(), mouseLoc.x)) { // upper left corner if (Kernel.isEqual(getBoundingBox().getRectangle().getMinY(), mouseLoc.y, 3)) { return EuclidianBoundingBoxHandler.TOP_LEFT; } // bottom left corner return EuclidianBoundingBoxHandler.BOTTOM_LEFT; } else if (Kernel.isEqual(getBoundingBox().getRectangle().getMaxX(), mouseLoc.x, 3)) { // upper right corner if (Kernel.isEqual(getBoundingBox().getRectangle().getMinY(), mouseLoc.y, 3)) { return EuclidianBoundingBoxHandler.TOP_RIGHT; } // bottom right corner return EuclidianBoundingBoxHandler.BOTTOM_RIGHT; } return EuclidianBoundingBoxHandler.UNDEFINED; } }