package org.geogebra.common.euclidian; import java.util.ArrayList; import org.geogebra.common.awt.GBasicStroke; import org.geogebra.common.awt.GColor; import org.geogebra.common.awt.GDimension; import org.geogebra.common.awt.GFont; import org.geogebra.common.awt.GFontRenderContext; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.awt.GPoint; import org.geogebra.common.awt.GRectangle; import org.geogebra.common.awt.font.GTextLayout; import org.geogebra.common.factories.AwtFactory; 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 gabor@gegeobra.org * * * Abstract class for EuclidianStatic */ public class EuclidianStatic { /** * need to clip just outside the viewing area when drawing eg vectors as a * near-horizontal thick vector isn't drawn correctly otherwise */ public static final int CLIP_DISTANCE = 5; /** standardstroke */ protected static final GBasicStroke standardStroke = AwtFactory .getPrototype().newMyBasicStroke(1.0f); /** stroke for selected geos */ protected static final GBasicStroke selStroke = AwtFactory.getPrototype() .newMyBasicStroke(1.0f + EuclidianStyleConstants.SELECTION_ADD); /** * @return default stroke */ static public GBasicStroke getDefaultStroke() { return standardStroke; } /** * @return stroke for selected geos */ static public GBasicStroke getDefaultSelectionStroke() { return selStroke; } // Michael Borcherds 2008-06-10 /** * @param str * string * @param font * font * @param frc * rendering context * @return text width */ public final static double textWidth(String str, GFont font, GFontRenderContext frc) { if ("".equals(str)) { return 0; } GTextLayout layout = AwtFactory.getPrototype().newTextLayout(str, font, frc); return layout.getAdvance(); } /** * Creates a stroke with thickness width, dashed according to line style * type. * * @param width * stroke width * @param type * stroke type (EuclidianStyleConstants.LINE_TYPE_*) * @return stroke */ public static GBasicStroke getStroke(double width, int type) { double[] dash; switch (type) { case EuclidianStyleConstants.LINE_TYPE_DOTTED: dash = new double[2]; dash[0] = width; // dot dash[1] = 3.0; // space break; case EuclidianStyleConstants.LINE_TYPE_DASHED_SHORT: dash = new double[2]; dash[0] = 4.0 + width; // short dash dash[1] = 4.0; // space break; case EuclidianStyleConstants.LINE_TYPE_DASHED_LONG: dash = new double[2]; dash[0] = 8.0 + width; // long dash dash[1] = 8.0; // space break; case EuclidianStyleConstants.LINE_TYPE_DASHED_DOTTED: dash = new double[4]; dash[0] = 8.0 + width; // dash dash[1] = 4.0; // space before dot dash[2] = width; // dot dash[3] = dash[1]; // space after dot break; default: // EuclidianStyleConstants.LINE_TYPE_FULL dash = null; } int endCap = dash != null ? GBasicStroke.CAP_BUTT : standardStroke.getEndCap(); return AwtFactory.getPrototype().newBasicStroke(width, endCap, standardStroke.getLineJoin(), standardStroke.getMiterLimit(), dash); } /* * public abstract float textWidth(String str, Font font, FontRenderContext * frc); */ /** * Draw a multiline LaTeX label. * * @param app * application * @param tempGraphics * temporary graphics * @param geo * geo * * @param g2 * graphics * @param font * font * @param fgColor * color * @param bgColor * background color * @param labelDesc * LaTeX text * @param xLabel * x-coord * @param yLabel * y-coord * @param serif * true touseserif font * @param callback * LaTeX loading callback * @return bounds of resulting LaTeX formula */ public static final GRectangle drawMultilineLaTeX(App app, GGraphics2D tempGraphics, GeoElementND geo, GGraphics2D g2, GFont font, GColor fgColor, GColor bgColor, String labelDesc, int xLabel, int yLabel, boolean serif, Runnable callback) { int fontSize = g2.getFont().getSize(); int lineSpread = (int) (fontSize * 1.0f); int lineSpace = (int) (fontSize * 0.5f); // latex delimiters \[ \] \( \) $$ -> $ // labelDesc = labelDesc.replaceAll( // "(\\$\\$|\\\\\\[|\\\\\\]|\\\\\\(|\\\\\\))", "\\$"); // split on $ but not \$ String[] elements = blockSplit(labelDesc); ArrayList<Integer> lineHeights = new ArrayList<Integer>(); lineHeights.add(lineSpread + lineSpace); ArrayList<Integer> elementHeights = new ArrayList<Integer>(); int depth = 0; // use latex by default just if there is just a single element boolean isLaTeX = (elements.length == 1); // calculate the required space of every element for (int i = 0, currentLine = 0; i < elements.length; ++i) { if (isLaTeX) { // save the height of this element by drawing it to a temporary // buffer GDimension dim = AwtFactory.getPrototype().newDimension(0, 0); dim = app.getDrawEquation().drawEquation(app, geo, tempGraphics, 0, 0, elements[i], font, ((GeoText) geo).isSerifFont(), fgColor, bgColor, false, false, callback); int height = dim.getHeight(); // depth += dim.depth; elementHeights.add(Integer.valueOf(height)); // check if this element is taller than every else in the line if (height > (lineHeights.get(currentLine)).intValue()) { lineHeights.set(currentLine, Integer.valueOf(height)); } } else { elements[i] = elements[i].replaceAll("\\\\\\$", "\\$"); String[] lines = elements[i].split("\\n", -1); for (int j = 0; j < lines.length; ++j) { elementHeights.add(Integer.valueOf(lineSpread)); // create a new line if (j + 1 < lines.length) { ++currentLine; lineHeights .add(Integer.valueOf(lineSpread + lineSpace)); } } } isLaTeX = !isLaTeX; } int width = 0; int height = 0; // use latex by default just if there is just a single element isLaTeX = (elements.length == 1); int xOffset = 0; int yOffset = 0; // now draw all elements for (int i = 0, currentLine = 0, currentElement = 0; i < elements.length; ++i) { if (elements[i] == null) { continue; } if (isLaTeX) { // calculate the y offset of this element by: (lineHeight - // elementHeight) / 2 yOffset = (((lineHeights.get(currentLine))).intValue() - ((elementHeights.get(currentElement))).intValue()) / 2; DrawEquation de = app.getDrawEquation(); // draw the equation and save the x offset xOffset += de.drawEquation(app, geo, g2, xLabel + xOffset, (yLabel + height) + yOffset, elements[i], font, ((GeoText) geo).isSerifFont(), fgColor, bgColor, true, false, callback).getWidth(); ++currentElement; } else { String[] lines = elements[i].split("\\n", -1); for (int j = 0; j < lines.length; ++j) { // calculate the y offset like done with the element yOffset = (((lineHeights.get(currentLine))).intValue() - ((elementHeights.get(currentElement))).intValue()) / 2; // draw the string g2.setFont(font); // JLaTeXMath changes g2's fontsize xOffset += drawIndexedString(app, g2, lines[j], xLabel + xOffset, yLabel + height + yOffset + lineSpread, serif).x; // add the height of this line if more lines follow if (j + 1 < lines.length) { height += ((lineHeights.get(currentLine))).intValue(); if (xOffset > width) { width = xOffset; } } // create a new line if more will follow if (j + 1 < lines.length) { ++currentLine; xOffset = 0; } ++currentElement; } } // last element, increase total height and check if this is the most // wide element if (i + 1 == elements.length) { height += ((lineHeights.get(currentLine))).intValue(); if (xOffset > width) { width = xOffset; } } isLaTeX = !isLaTeX; } return AwtFactory.getPrototype().newRectangle(xLabel - 3, yLabel - 3 + depth, width + 6, height + 6); } /** * eg FormulaText["\text{Price (\$)}"] * * @param str * String to split * @return str split on $ but not \$ */ private static String[] blockSplit(String str) { // http://stackoverflow.com/questions/2709839/how-do-i-express-but-not-preceded-by-in-a-java-regular-expression // negative lookbehind // return str.split("(?<!\\\\)$"); // JavaScript GWT compatible version // reverse string and use a lookahead // http://stackoverflow.com/questions/641407/javascript-negative-lookbehind-equivalent String reverse = new StringBuilder(str).reverse().toString(); String[] split = reverse.split("\\$(?!([\\\\]))"); // special case: need an extra "" at the start if (str.startsWith("$")) { String[] normal = new String[split.length + 1]; normal[0] = ""; for (int i = 0; i < split.length; i++) { normal[split.length - i] = new StringBuilder(split[i]).reverse() .toString(); } return normal; } String[] normal = new String[split.length]; for (int i = 0; i < split.length; i++) { normal[split.length - i - 1] = new StringBuilder(split[i]).reverse() .toString(); } return normal; } private static GFont getIndexFont(GFont f) { // index font size should be at least 8pt int newSize = Math.max((int) (f.getSize() * 0.9), 8); return f.deriveFont(f.getStyle(), newSize); } /** * Always draws a string str with possible indices to g2 at position x, y. * The indices are drawn using the given indexFont. Examples for strings * with indices: "a_1" or "s_{ab}" * * @param app * application * @param g3 * graphics * * @param str * input string * @param xPos * x-coord * @param yPos * y-coord * @param serif * true to use serif font * @return additional pixel needed to draw str (x-offset, y-offset) */ public static GPoint drawIndexedString(App app, GGraphics2D g3, String str, double xPos, double yPos, boolean serif) { return drawIndexedString(app, g3, str, xPos, yPos, serif, true); } /** * Draws or just measures the string str with possible indices to g2. The * indices are drawn using the given indexFont. Examples for strings with * indices: "a_1" or "s_{ab}" * * @param app * application * @param g3 * graphics * * @param str * input string * @param xPos * x-coord * @param yPos * y-coord * @param serif * true to use serif font * @param doDraw * true to draw, false to measure only. * @return additional pixel needed to draw str (x-offset, y-offset) */ public static GPoint drawIndexedString(App app, GGraphics2D g3, String str, double xPos, double yPos, boolean serif, boolean doDraw) { GFont g2font = g3.getFont(); g2font = app.getFontCanDisplay(str, serif, g2font.getStyle(), g2font.getSize()); GFont indexFont = getIndexFont(g2font); GFont font = g2font; // GTextLayout layout; GFontRenderContext frc = g3.getFontRenderContext(); int indexOffset = indexFont.getSize() / 2; double maxY = 0; int depth = 0; double x = xPos; double y = yPos; int startPos = 0; if (str == null) { return null; } int length = str.length(); for (int i = 0; i < length; i++) { switch (str.charAt(i)) { default: // do nothing break; case '_': // draw everything before _ if (i > startPos) { font = (depth == 0) ? g2font : indexFont; y = yPos + depth * indexOffset; if (y > maxY) { maxY = y; } String tempStr = str.substring(startPos, i); if (doDraw) { g3.setFont(font); g3.drawString(tempStr, x, y); } x += measureString(tempStr, font, frc); } startPos = i + 1; depth++; // check if next character is a '{' (beginning of index with // several chars) if (startPos < length && str.charAt(startPos) != '{') { font = (depth == 0) ? g2font : indexFont; y = yPos + depth * indexOffset; if (y > maxY) { maxY = y; } String tempStr = str.substring(startPos, startPos + 1); if (doDraw) { g3.setFont(font); g3.drawString(tempStr, x, y); } x += measureString(tempStr, font, frc); depth--; } i++; startPos++; break; case '}': // end of index with several characters if (depth > 0) { if (i > startPos) { font = indexFont; y = yPos + depth * indexOffset; if (y > maxY) { maxY = y; } String tempStr = str.substring(startPos, i); if (doDraw) { g3.setFont(font); g3.drawString(tempStr, x, y); } x += measureString(tempStr, font, frc); } startPos = i + 1; depth--; } break; } } if (startPos < length) { font = (depth == 0) ? g2font : indexFont; y = yPos + depth * indexOffset; if (y > maxY) { maxY = y; } String tempStr = str.substring(startPos); if (doDraw) { g3.setFont(font); g3.drawString(tempStr, x, y); } x += measureString(tempStr, font, frc); } if (doDraw) { g3.setFont(g2font); } return new GPoint((int) Math.round(x - xPos), (int) Math.round(maxY - yPos)); } private static double measureString(String tempStr, GFont font, GFontRenderContext frc) { if (frc != null) { return AwtFactory.getPrototype().newTextLayout(tempStr, font, frc) .getAdvance(); } return StringUtil.getPrototype().estimateLength(tempStr, font); } /** * @param app * application * @param labelDesc * text * @param xLabel * x-coord * @param yLabel * y-coord * @param g2 * graphics * @param serif * true for serif font * @param textFont * font * @return border of resulting text drawing */ public final static GRectangle drawMultiLineText(App app, String labelDesc, int xLabel, int yLabel, GGraphics2D g2, boolean serif, GFont textFont) { int lines = 0; int fontSize = textFont.getSize(); double lineSpread = fontSize * 1.5f; GFont font = app.getFontCanDisplay(labelDesc, serif, textFont.getStyle(), fontSize); GFontRenderContext frc = g2.getFontRenderContext(); int xoffset = 0; // draw text line by line int lineBegin = 0; int length = labelDesc.length(); for (int i = 0; i < length - 1; i++) { if (labelDesc.charAt(i) == '\n') { // iOS (bug?) - bold text needs font setting for each line g2.setFont(font); // end of line reached: draw this line g2.drawString(labelDesc.substring(lineBegin, i), xLabel, yLabel + lines * lineSpread); int width = (int) textWidth(labelDesc.substring(lineBegin, i), font, frc); if (width > xoffset) { xoffset = width; } lines++; lineBegin = i + 1; } } double ypos = yLabel + lines * lineSpread; // iOS (bug?) - bold text needs font setting for each line g2.setFont(font); g2.drawString(labelDesc.substring(lineBegin), xLabel, ypos); int width = (int) textWidth(labelDesc.substring(lineBegin), font, frc); if (width > xoffset) { xoffset = width; } // Michael Borcherds 2008-06-10 // changed setLocation to setBounds (bugfix) // and added final float textWidth() // labelRectangle.setLocation(xLabel, yLabel - fontSize); int height = (int) ((lines + 1) * lineSpread); return AwtFactory.getPrototype().newRectangle(xLabel - 3, yLabel - fontSize - 3, xoffset + 6, height + 6); } public static boolean drawIndexedMultilineString(App app, String labelDesc, GGraphics2D g2, GRectangle labelRectangle, GFont textFont, boolean serif, int xLabel, int yLabel) { // draw text line by line int lineBegin = 0; int lines = 0; int fontSize = textFont.getSize(); double lineSpread = fontSize * 1.5; int length = labelDesc.length(); int xoffset = 0, yoffset = 0; for (int i = 0; i < length - 1; i++) { if (labelDesc.charAt(i) == '\n') { // end of line reached: draw this line // iOS (bug?) - bold text needs font setting for each line g2.setFont(textFont); GPoint p = EuclidianStatic.drawIndexedString( app, g2, labelDesc.substring(lineBegin, i), xLabel, yLabel + lines * lineSpread, serif); if (p.x > xoffset) { xoffset = p.x; } if (p.y > yoffset) { yoffset = p.y; } lines++; lineBegin = i + 1; } } double ypos = yLabel + lines * lineSpread; // iOS (bug?) - bold text needs font setting for each line g2.setFont(textFont); GPoint p = EuclidianStatic.drawIndexedString(app, g2, labelDesc.substring(lineBegin), xLabel, ypos, serif); if (p.x > xoffset) { xoffset = p.x; } if (p.y > yoffset) { yoffset = p.y; } int height = (int) ((lines + 1) * lineSpread); labelRectangle.setBounds(xLabel - 3, yLabel - fontSize - 3, xoffset + 6, height + 6); return yoffset > 0; } }