/* 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. */ /* * Drawable.java * * Created on 13. Oktober 2001, 17:40 */ package org.geogebra.common.euclidian; import org.geogebra.common.awt.GArea; import org.geogebra.common.awt.GBasicStroke; import org.geogebra.common.awt.GBufferedImage; import org.geogebra.common.awt.GColor; import org.geogebra.common.awt.GDimension; import org.geogebra.common.awt.GFont; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.awt.GPaint; import org.geogebra.common.awt.GPoint; import org.geogebra.common.awt.GRectangle; import org.geogebra.common.awt.GShape; import org.geogebra.common.awt.font.GTextLayout; import org.geogebra.common.euclidian.event.AbstractEvent; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoText; import org.geogebra.common.kernel.kernelND.GeoElementND; import org.geogebra.common.main.App; import org.geogebra.common.plugin.EuclidianStyleConstants; import org.geogebra.common.util.StringUtil; /** * * @author Markus */ public abstract class Drawable extends DrawableND { private boolean forceNoFill; /** * Default stroke for this drawable */ protected GBasicStroke objStroke = EuclidianStatic.getDefaultStroke(); /** * Stroke for this drawable in case referenced geo is selected */ protected GBasicStroke selStroke = EuclidianStatic .getDefaultSelectionStroke(); /** * Stroke for decorations; always full */ protected GBasicStroke decoStroke = EuclidianStatic.getDefaultStroke(); private int lineThickness = -1; private int lineType = -1; /** * View in which this is drawn */ protected EuclidianView view; /** * Referenced GeoElement */ protected GeoElement geo; /** x-coord of the label */ public int xLabel; /** y-coord of the label */ public int yLabel; /** label Description */ public String labelDesc; private String oldLabelDesc; private boolean labelHasIndex = false; /** for label hit testing */ protected GRectangle labelRectangle = AwtFactory.getPrototype() .newRectangle(0, 0); /** * Stroked shape for hits testing of conics, loci ... with alpha = 0 */ protected GShape strokedShape; /** * Stroked shape for hits testing of hyperbolas */ protected GShape strokedShape2; private GArea shape; private int lastFontSize = -1; /** tracing */ protected boolean isTracing = false; // boolean createdByDrawList = false; @Override public abstract void update(); /** * Draws this drawable to given graphics * * @param g2 * graphics */ public abstract void draw(GGraphics2D g2); /** * @param x * mouse x-coord * @param y * mouse y-coord * @param hitThreshold * pixel threshold * @return true if hit */ public abstract boolean hit(int x, int y, int hitThreshold); /** * @param rect * rectangle * @return true if the whole drawable is inside */ public abstract boolean isInside(GRectangle rect); /** * @param rect * rectangle * @return true if a part of this Drawable is within the rectangle */ public boolean intersectsRectangle(GRectangle rect) { GArea s = getShape(); if (s == null) { return false; } if (geo.isFilled()) { return s.intersects(rect); } return s.intersects(rect) && !s.contains(rect); } @Override public abstract GeoElement getGeoElement(); /** * @param geo * referenced geo */ public abstract void setGeoElement(GeoElement geo); /** * @return bounding box construction */ public abstract BoundingBox getBoundingBox(); /** * update bounding box construction */ public abstract void updateBoundingBox(); @Override public double getxLabel() { return xLabel; } @Override public double getyLabel() { return yLabel; } /** * Updates font size */ public void updateFontSize() { // do nothing, overriden in drawables } /** * Returns the bounding box of this Drawable in screen coordinates. * * @return null when this Drawable is infinite or undefined */ public GRectangle getBounds() { return null; } /** * Draws label of referenced geo * * @param g2 * graphics */ public final void drawLabel(GGraphics2D g2) { if (labelDesc == null) { return; } String label = labelDesc; // stripping off helper syntax from captions // assuming that non-caption labels will not contain // that helper syntax anyway int ind = label.indexOf("%style="); if (ind > -1) { label = label.substring(0, ind); } GFont oldFont = null; // allow LaTeX caption surrounded by $ $ if (label.length() > 1 && (label.charAt(0) == '$') && label.endsWith("$")) { boolean serif = true; // nice "x"s if (geo.isGeoText()) { serif = ((GeoText) geo).isSerifFont(); } int offsetY = 10 + view.getFontSize(); // make sure LaTeX labels // don't go // off bottom of screen App app = view.getApplication(); GDimension dim = app.getDrawEquation().drawEquation( geo.getKernel().getApplication(), geo, g2, xLabel, yLabel - offsetY, label.substring(1, label.length() - 1), g2.getFont(), serif, g2.getColor(), g2.getBackground(), true, false, null); labelRectangle.setBounds(xLabel, yLabel - offsetY, dim.getWidth(), dim.getHeight()); return; } // label changed: check for bold or italic tags in caption if (!labelDesc.equals(oldLabelDesc) || (labelDesc.length() > 0 && labelDesc.charAt(0) == '<')) { boolean italic = false; // support for bold and italic tags in captions // must be whole caption if (label.startsWith("<i>") && label.endsWith("</i>")) { oldFont = g2.getFont(); // use Serif font so that we can get a nice curly italic x g2.setFont(view.getApplication().getFontCommon(true, oldFont.getStyle() | GFont.ITALIC, oldFont.getSize())); label = label.substring(3, label.length() - 4); italic = true; } if (label.startsWith("<b>") && label.endsWith("</b>")) { oldFont = g2.getFont(); g2.setFont(g2.getFont() .deriveFont(GFont.BOLD + (italic ? GFont.ITALIC : 0))); label = label.substring(3, label.length() - 4); } } // no index in label: draw it fast int fontSize = g2.getFont().getSize(); if (labelDesc.equals(oldLabelDesc) && !labelHasIndex && lastFontSize == fontSize) { g2.drawString(label, xLabel, yLabel); labelRectangle.setLocation(xLabel, yLabel - fontSize); } else { // label with index or label has changed: // do the slower index drawing routine and check for indices oldLabelDesc = labelDesc; GPoint p = EuclidianStatic.drawIndexedString(view.getApplication(), g2, label, xLabel, yLabel, isSerif()); labelHasIndex = p.y > 0; labelRectangle.setBounds(xLabel, yLabel - fontSize, p.x, fontSize + p.y); lastFontSize = fontSize; } if (oldFont != null) { g2.setFont(oldFont); } } /** * Adapts xLabel and yLabel to make sure that the label rectangle fits fully * on screen. * * @param Xmultiplier * multiply the x size by it to ensure fitting (default: 1.0) * @param Ymultiplier * multiply the y size by it to ensure fitting (default: 1.0) */ private void ensureLabelDrawsOnScreen(double Xmultiplier, double Ymultiplier, GFont font) { // draw label and int widthEstimate = (int) labelRectangle.getWidth(); int heightEstimate = (int) labelRectangle.getHeight(); boolean roughEstimate = false; if (!labelDesc.equals(oldLabelDesc) || lastFontSize != font.getSize()) { if (labelDesc.startsWith("$")) { // for LaTeX we need proper repaint drawLabel(view.getTempGraphics2D(font)); widthEstimate = (int) labelRectangle.getWidth(); heightEstimate = (int) labelRectangle.getHeight(); } else { // if we use name = value, this may still be called pretty // often. // Hence use heuristic here instead of measurement heightEstimate = (int) (StringUtil.getPrototype() .estimateHeight(labelDesc, font) * Ymultiplier); widthEstimate = (int) (StringUtil.getPrototype() .estimateLengthHTML(labelDesc, font) * Xmultiplier); roughEstimate = true; } } // make sure labelRectangle fits on screen horizontally if (xLabel < 3) { xLabel = 3; } else if (xLabel > view.getWidth() - widthEstimate - 3) { if (roughEstimate) { drawLabel(view.getTempGraphics2D(font)); widthEstimate = (int) labelRectangle.getHeight(); heightEstimate = (int) labelRectangle.getHeight(); roughEstimate = false; } xLabel = Math.min(xLabel, view.getWidth() - widthEstimate - 3); } if (yLabel < heightEstimate) { if (roughEstimate) { drawLabel(view.getTempGraphics2D(font)); heightEstimate = (int) labelRectangle.getHeight(); } yLabel = Math.max(yLabel, heightEstimate); } else { yLabel = Math.min(yLabel, view.getHeight() - 3); } // update label rectangle position labelRectangle.setLocation(xLabel, yLabel - view.getFontSize()); } /** * @param g2 * graphics * @param font * font * @param fgColor * text color * @param bgColor * background color */ public final void drawMultilineLaTeX(GGraphics2D g2, GFont font, GColor fgColor, GColor bgColor) { labelRectangle.setBounds( EuclidianStatic.drawMultilineLaTeX(view.getApplication(), view.getTempGraphics2D(font), geo, g2, font, fgColor, bgColor, labelDesc, xLabel, yLabel, isSerif(), null)); } /** * @return true if serif font is used for GeoText */ final boolean isSerif() { return geo.isGeoText() && ((GeoText) geo).isSerifFont(); } /** * @param g2 * graphics * @param textFont * font */ protected final void drawMultilineText(GGraphics2D g2, GFont textFont) { if (labelDesc == null) { return; } // no index in text if (labelDesc.equals(oldLabelDesc) && !labelHasIndex) { labelRectangle.setBounds(EuclidianStatic.drawMultiLineText( view.getApplication(), labelDesc, xLabel, yLabel, g2, isSerif(), textFont)); } else { // text with indices // label description has changed, search for possible indices oldLabelDesc = labelDesc; labelHasIndex = EuclidianStatic .drawIndexedMultilineString(view.getApplication(), labelDesc, g2, labelRectangle, textFont, isSerif(), xLabel, yLabel); } } /** * Adds geo's label offset to xLabel and yLabel. * * @return whether the label fits on screen */ final protected boolean addLabelOffset() { if (geo.labelOffsetX == 0 && geo.labelOffsetY == 0) { return false; } int x = xLabel + geo.labelOffsetX; int y = yLabel + geo.labelOffsetY; // don't let offset move label out of screen int xmax = view.getWidth() - 15; int ymax = view.getHeight() - 5; if (x < 5 || x > xmax) { return false; } if (y < 15 || y > ymax) { return false; } xLabel = x; yLabel = y; return true; } /** * Adds geo's label offset to xLabel and yLabel. * * @param font * used font * */ public final void addLabelOffsetEnsureOnScreen(GFont font) { addLabelOffsetEnsureOnScreen(1.0, 1.0, font); } /** * Adds geo's label offset to xLabel and yLabel. * * @param Xmultiplier * multiply the x size by it to ensure fitting * @param Ymultiplier * multiply the y size by it to ensure fitting * @param font * font * */ public final void addLabelOffsetEnsureOnScreen(double Xmultiplier, double Ymultiplier, GFont font) { // MAKE SURE LABEL STAYS ON SCREEN xLabel += geo.labelOffsetX; yLabel += geo.labelOffsetY; // change xLabel and yLabel so that label stays on screen ensureLabelDrawsOnScreen(Xmultiplier, Ymultiplier, font); } /** * Was the label clicked at? (mouse pointer location (x,y) in screen coords) * * @param x * mouse x-coord * @param y * mouse y-coord * @return true if hit */ public boolean hitLabel(int x, int y) { return labelRectangle.contains(x, y); } /** * Was clicked at the handlers of bounding box? (mouse pointer location * (x,y) in screen coords) * * @param x * mouse x-coord * @param y * mouse y-coord * @param hitThreshold * - threshold * @return bounding box handler */ public EuclidianBoundingBoxHandler hitBoundingBoxHandler(int x, int y, int hitThreshold) { int hit = -1; if (getBoundingBox() != null && getBoundingBox() == view.getBoundingBox()) { hit = getBoundingBox().hitHandlers(x, y, hitThreshold); } switch (hit) { case 0: return EuclidianBoundingBoxHandler.TOP_LEFT; case 1: return EuclidianBoundingBoxHandler.BOTTOM_LEFT; case 2: return EuclidianBoundingBoxHandler.BOTTOM_RIGHT; case 3: return EuclidianBoundingBoxHandler.TOP_RIGHT; case 4: return EuclidianBoundingBoxHandler.TOP; case 5: return EuclidianBoundingBoxHandler.LEFT; case 6: return EuclidianBoundingBoxHandler.BOTTOM; case 7: return EuclidianBoundingBoxHandler.RIGHT; default: return EuclidianBoundingBoxHandler.UNDEFINED; } } private boolean forcedLineType; private HatchingHandler hatchingHandler; /** * Set fixed line type and ignore line type of the geo. Needed for * inequalities. * * @param type * line type */ public final void forceLineType(int type) { forcedLineType = true; lineType = type; } /** * Update strokes (default,selection,deco) accordingly to geo * * @param fromGeo * geo whose style should be used for the update */ final public void updateStrokes(GeoElementND fromGeo) { updateStrokes(fromGeo, 0); } /** * Update strokes (default,selection,deco) accordingly to geo * * @param fromGeo * geo whose style should be used for the update * @param minThickness * minimal acceptable thickness */ final public void updateStrokes(GeoElementND fromGeo, int minThickness) { strokedShape = null; strokedShape2 = null; if (lineThickness != fromGeo.getLineThickness()) { lineThickness = Math.max(minThickness, fromGeo.getLineThickness()); if (!forcedLineType) { lineType = fromGeo.getLineType(); } double width = lineThickness / 2.0; objStroke = EuclidianStatic.getStroke(width, lineType); decoStroke = EuclidianStatic.getStroke(width, EuclidianStyleConstants.LINE_TYPE_FULL); selStroke = EuclidianStatic.getStroke( width + EuclidianStyleConstants.SELECTION_ADD, EuclidianStyleConstants.LINE_TYPE_FULL); } else if (lineType != fromGeo.getLineType()) { if (!forcedLineType) { lineType = fromGeo.getLineType(); } double width = lineThickness / 2.0; objStroke = EuclidianStatic.getStroke(width, lineType); } } /** * Update strokes (default,selection,deco) accordingly to geo; ignores line * style * * @param fromGeo * geo whose style should be used for the update */ public final void updateStrokesJustLineThickness(GeoElement fromGeo) { strokedShape = null; strokedShape2 = null; if (lineThickness != fromGeo.getLineThickness()) { lineThickness = fromGeo.getLineThickness(); double width = lineThickness / 2.0; objStroke = AwtFactory.getPrototype().newBasicStroke(width, objStroke.getEndCap(), objStroke.getLineJoin(), objStroke.getMiterLimit(), objStroke.getDashArray()); decoStroke = AwtFactory.getPrototype().newBasicStroke(width, objStroke.getEndCap(), objStroke.getLineJoin(), objStroke.getMiterLimit(), decoStroke.getDashArray()); selStroke = AwtFactory.getPrototype().newBasicStroke( width + EuclidianStyleConstants.SELECTION_ADD, objStroke.getEndCap(), objStroke.getLineJoin(), objStroke.getMiterLimit(), selStroke.getDashArray()); } } /** * Fills given shape * * @param g2 * graphics * @param fillShape * shape to be filled */ protected void fill(GGraphics2D g2, GShape fillShape) { fill(g2, fillShape, null, null); } /** * Fills given shape * * @param g2 * graphics * @param fillShape * shape to be filled * @param gpaint0 * override paint * @param subImage * override image */ protected void fill(GGraphics2D g2, GShape fillShape, GPaint gpaint0, GBufferedImage subImage) { if (isForceNoFill()) { return; } GPaint gpaint = gpaint0; if (geo.isHatchingEnabled() || gpaint != null) { // use decoStroke as it is always full (not dashed/dotted etc) if (gpaint == null) { gpaint = getHatchingHandler().setHatching(g2, decoStroke, geo.getObjectColor(), geo.getBackgroundColor(), geo.getAlphaValue(), geo.getHatchingDistance(), geo.getHatchingAngle(), geo.getFillType(), geo.getFillSymbol(), geo.getKernel().getApplication()); } g2.setPaint(gpaint); if (!geo.getKernel().getApplication().isHTML5Applet()) { g2.fill(fillShape); } else { GBufferedImage subImage2 = subImage; if (subImage2 == null) { subImage2 = getHatchingHandler().getSubImage(); } // take care of filling after the image is loaded AwtFactory.getPrototype().fillAfterImageLoaded(fillShape, g2, subImage2, geo.getKernel().getApplication()); } } else if (geo.getFillType() == GeoElement.FillType.IMAGE) { getHatchingHandler().setTexture(g2, geo, geo.getAlphaValue()); g2.fill(fillShape); } else if (geo.getAlphaValue() > 0.0f) { g2.setPaint(geo.getFillColor()); // magic for switching off dash emulation moved to GGraphics2DW g2.fill(fillShape); } } private HatchingHandler getHatchingHandler() { if (hatchingHandler == null) { hatchingHandler = new HatchingHandler(); } return hatchingHandler; } /** * @param forceNoFill * the forceNoFill to set */ public void setForceNoFill(boolean forceNoFill) { this.forceNoFill = forceNoFill; } /** * @return the forceNoFill */ public boolean isForceNoFill() { return forceNoFill; } /** * @param shape * the shape to set */ public final void setShape(GArea shape) { this.shape = shape; } /** * @return the shape */ public GArea getShape() { return shape; } @Override public boolean isTracing() { return isTracing; } /** * draw trace of this geo into given Graphics2D * * @param g2 * graphics */ protected void drawTrace(GGraphics2D g2) { // do nothing, overridden where needed } /** * @return view in which this is drawn */ public EuclidianView getView() { return view; } /** * @return whether geo is visible in euclidian views */ public final boolean isEuclidianVisible() { return geo.isEuclidianVisible(); } /** * @return If the {@code GeoElement} has line opacity then a {@code GColor} * object with the alpha value set, else the original {@code GColor} * . */ protected GColor getObjectColor() { GColor color = geo.getObjectColor(); if (geo.hasLineOpacity()) { color = GColor.newColor(color.getRed(), color.getGreen(), color.getBlue(), geo.getLineOpacity()); } return color; } /** * Update when view was changed and geo is still the same */ public void updateForView() { update(); } /** * method to handle corner or side drag of bounding box to resize geo * * @param e * - mouse drag event * @param handler * - which corner was dragged */ public void updateByBoundingBoxResize(AbstractEvent e, EuclidianBoundingBoxHandler handler) { // do nothing here } /** * method to update geoElement of drawable by drag of resize handlers of * boundingBox * * @param e * - mouse release event */ public void updateGeo(AbstractEvent e) { // do nothing here } public int measureTextWidth(String text, GFont font, GGraphics2D g2) { GTextLayout layout = getTextLayout(text, font, g2); return layout != null ? (int) layout.getAdvance() : 0; } public GTextLayout getTextLayout(String text, GFont font, GGraphics2D g2) { if (text == null || text.isEmpty()) { return null; } return AwtFactory.getPrototype().newTextLayout(text, font, g2.getFontRenderContext()); } }