package org.geogebra.common.euclidian; import java.util.ArrayList; import org.geogebra.common.awt.GFont; import org.geogebra.common.awt.GFontRenderContext; import org.geogebra.common.awt.GGeneralPath; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.awt.font.GTextLayout; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.arithmetic.ExpressionNode; import org.geogebra.common.main.Feature; import org.geogebra.common.plugin.EuclidianStyleConstants; import org.geogebra.common.util.MyMath; /** * Draws axes in 2D */ public class DrawAxis { /** view */ EuclidianView view; private GGeneralPath gp; /** * @param euclidianView * view */ public DrawAxis(EuclidianView euclidianView) { this.view = euclidianView; } /** * @param g2 * graphics */ protected void drawAxes(GGraphics2D g2) { // TRAC-5292 // problem exporting to PDF // GGB-766 // also needed for Braille char minusSign = view.getApplication().getExportType() .getAxisMinusSign(); // xCrossPix: yAxis crosses the xAxis at this x pixel double xCrossPix = view.getXAxisCrossingPixel(); // yCrossPix: xAxis crosses the yAxis at this y pixel double yCrossPix = view.getYAxisCrossingPixel(); // yAxis end value (for drawing half-axis) int yAxisEnd = view.positiveAxes[1] ? (int) yCrossPix : view.getHeight(); // xAxis start value (for drawing half-axis) int xAxisStart = view.positiveAxes[0] ? (int) xCrossPix : 0; // for axes ticks boolean bold = view.areAxesBold(); boolean filled = (view.axesLineType & EuclidianStyleConstants.AXES_FILL_ARROWS) != 0; if (filled && gp == null) { gp = AwtFactory.getPrototype().newGeneralPath(); } boolean drawRightArrow = ((view.axesLineType & EuclidianStyleConstants.AXES_RIGHT_ARROW) != 0) && !(view.positiveAxes[0] && (view.getXmax() < view.axisCross[1])); boolean drawTopArrow = ((view.axesLineType & EuclidianStyleConstants.AXES_RIGHT_ARROW) != 0) && !(view.positiveAxes[1] && (view.getYmax() < view.axisCross[0])); boolean drawLeftArrow = ((view.axesLineType & EuclidianStyleConstants.AXES_LEFT_ARROW) != 0) && !(view.positiveAxes[0]); boolean drawBottomArrow = ((view.axesLineType & EuclidianStyleConstants.AXES_LEFT_ARROW) != 0) && !(view.positiveAxes[1]); // AXES_TICK_STYLE_MAJOR_MINOR = 0; // AXES_TICK_STYLE_MAJOR = 1; // AXES_TICK_STYLE_NONE = 2; g2.setFont(view.getFontAxes()); int fontsize = view.getFontAxes().getSize(); int arrowSize = fontsize / 3; g2.setPaint(view.axesColor); GFontRenderContext frc = g2.getFontRenderContext(); if (bold) { view.axesStroke = EuclidianView.boldAxesStroke; view.tickStroke = EuclidianView.boldAxesStroke; arrowSize += 1; } else { view.axesStroke = EuclidianView.defAxesStroke; view.tickStroke = EuclidianView.defAxesStroke; } // make sure arrows don't go off screen (eg EMF export) double arrowAdjustx = drawRightArrow ? view.axesStroke.getLineWidth() : 0; // Draw just y-axis first (in case any labels need to be drawn over it) if (view.yAxisOnscreen()) { predrawYAxis(g2, xCrossPix, arrowSize, filled, drawTopArrow, drawBottomArrow, yAxisEnd); // erase grid to make space for labels } // ======================================== // X-AXIS if (view.xAxisOnscreen() && !view.getApplication().has(Feature.TICK_NUMBERS_AT_EDGE) || view.showAxes[0] && view.getApplication() .has(Feature.TICK_NUMBERS_AT_EDGE)) { // erase the grid to make space for labels, use two rectangles // label of x axis if (view.axesLabels[0] != null) { GFont font = view.getFontLine() .deriveFont(view.axesLabelsStyle[0]); GTextLayout layout = AwtFactory.getPrototype() .newTextLayout(view.axesLabels[0], font, frc); if (!view.axesLabels[0].contains("_")) { layout.draw(g2, (int) (view.getWidth() - 10 - layout.getAdvance()), (int) (yCrossPix - 4)); } else { GFont old = g2.getFont(); g2.setFont(font); EuclidianStatic.drawIndexedString(view.getApplication(), g2, view.axesLabels[0], view.getWidth() - 10 - layout.getAdvance(), (int) (yCrossPix - 4), false); g2.setFont(old); } } // numbers // x-Axis itself g2.setStroke(view.axesStroke); g2.drawStraightLine(xAxisStart + (drawLeftArrow ? 2 : 0), yCrossPix, view.getWidth() - arrowAdjustx - 1, yCrossPix); if (drawRightArrow) { if (filled) { gp.reset(); gp.moveTo((view.getWidth() - arrowAdjustx), yCrossPix); gp.lineTo( (view.getWidth() - arrowAdjustx - arrowSize * 4), (yCrossPix - arrowSize)); gp.lineTo( (view.getWidth() - arrowAdjustx - arrowSize * 4), (yCrossPix + arrowSize)); g2.fill(gp); } else { // draw right arrow for x-axis g2.drawStraightLine(view.getWidth() - arrowAdjustx, yCrossPix, view.getWidth() - arrowAdjustx - arrowSize, yCrossPix - arrowSize); g2.drawStraightLine(view.getWidth() - arrowAdjustx, yCrossPix, view.getWidth() - arrowAdjustx - arrowSize, yCrossPix + arrowSize); } } if (drawLeftArrow) { if (filled) { gp.reset(); gp.moveTo((arrowAdjustx), yCrossPix); gp.lineTo((arrowAdjustx + arrowSize * 4), (yCrossPix - arrowSize)); gp.lineTo((arrowAdjustx + arrowSize * 4), (yCrossPix + arrowSize)); g2.fill(gp); } else { // draw left arrow for x-axis g2.drawStraightLine(arrowAdjustx, yCrossPix, arrowAdjustx + arrowSize, yCrossPix - arrowSize); g2.drawStraightLine(arrowAdjustx, yCrossPix, arrowAdjustx + arrowSize, yCrossPix + arrowSize); } } view.axesNumberingDistances[0] = Kernel .checkDecimalFraction(view.axesNumberingDistances[0]); if (view.logAxes[0]) { drawXTicksLog(g2, yCrossPix, minusSign, drawRightArrow, fontsize, xAxisStart); } else { drawXTicksLinear(g2, yCrossPix, minusSign, drawRightArrow, fontsize, xAxisStart); } } // ======================================== // Y-AXIS if (view.yAxisOnscreen() && !view.getApplication().has(Feature.TICK_NUMBERS_AT_EDGE) || view.showAxes[1] && view.getApplication() .has(Feature.TICK_NUMBERS_AT_EDGE)) { // label of y axis if (view.axesLabels[1] != null) { GFont font = view.getFontLine() .deriveFont(view.axesLabelsStyle[1]); GTextLayout layout = AwtFactory.getPrototype() .newTextLayout(view.axesLabels[1], font, frc); if (!view.axesLabels[1].contains("_")) { layout.draw(g2, (int) (xCrossPix + 5), (int) (5 + layout.getAscent())); } else { GFont old = g2.getFont(); g2.setFont(font); EuclidianStatic.drawIndexedString(view.getApplication(), g2, view.axesLabels[1], (int) (xCrossPix + 5), (int) (5 + layout.getAscent()), false); g2.setFont(old); } } if (view.logAxes[1]) { drawYticksLog(g2, xCrossPix, fontsize, minusSign, drawTopArrow, yCrossPix, yAxisEnd); } else { drawYticksLinear(g2, xCrossPix, fontsize, minusSign, drawTopArrow, yCrossPix, yAxisEnd); } } } private void predrawYAxis(GGraphics2D g2, double xCrossPix, double arrowSize, boolean filled, boolean drawTopArrow, boolean drawBottomArrow, double yAxisEnd) { double arrowAdjusty = drawTopArrow ? view.axesStroke.getLineWidth() : 0; // y-Axis itself g2.setStroke(view.axesStroke); g2.drawStraightLine(xCrossPix, arrowAdjusty + (drawTopArrow ? 1 : -1), xCrossPix, yAxisEnd + (drawBottomArrow ? -2 : 0)); if (drawTopArrow) { if (filled) { gp.reset(); gp.moveTo(xCrossPix, arrowAdjusty); gp.lineTo((xCrossPix - arrowSize), (arrowAdjusty + 4 * arrowSize)); gp.lineTo((xCrossPix + arrowSize), (arrowAdjusty + 4 * arrowSize)); g2.fill(gp); } else { // draw top arrow for y-axis g2.drawStraightLine(xCrossPix, arrowAdjusty, xCrossPix - arrowSize, arrowAdjusty + arrowSize); g2.drawStraightLine(xCrossPix, arrowAdjusty, xCrossPix + arrowSize, arrowAdjusty + arrowSize); } } if (drawBottomArrow) { if (filled) { gp.reset(); gp.moveTo(xCrossPix, (view.getHeight() - arrowAdjusty)); gp.lineTo((xCrossPix - arrowSize), (view.getHeight() - arrowAdjusty - 4 * arrowSize)); gp.lineTo((xCrossPix + arrowSize), (view.getHeight() - arrowAdjusty - 4 * arrowSize)); g2.fill(gp); } else { // draw bottom arrow for y-axis g2.drawStraightLine(xCrossPix, view.getHeight() - arrowAdjusty, xCrossPix - arrowSize, view.getHeight() - arrowAdjusty - arrowSize); g2.drawStraightLine(xCrossPix, view.getHeight() - arrowAdjusty, xCrossPix + arrowSize, view.getHeight() - arrowAdjusty - arrowSize); } } } private void drawYticksLinear(GGraphics2D g2, double xCrossPix, int fontsize, char minusSign, boolean drawTopArrow, double yCrossPix, double yAxisEnd) { double xoffset = -4 - (fontsize / 4); double yoffset = (fontsize / 2) - 1; boolean[] drawMajorTicks = { view.axesTickStyles[0] <= 1, view.axesTickStyles[1] <= 1 }; boolean[] drawMinorTicks = { view.axesTickStyles[0] == 0, view.axesTickStyles[1] == 0 }; double xSmall1 = xCrossPix - 0; double xSmall2 = xCrossPix - 2; double xBig = xCrossPix - 3; double smallTickOffset = 0; double xZeroTick = xCrossPix; if (view.areAxesBold()) { xSmall2--; } // numbers double rw = view.getYmin() - (view.getYmin() % view.axesNumberingDistances[1]); long labelno = Math.round(rw / view.axesNumberingDistances[1]); // by default we start with minor tick to the left of first major // tick, exception is for positive only double axesStep = view.getYscale() * view.axesNumberingDistances[1]; // pixelstep if (view.getPositiveAxes()[1] && (Kernel.isGreaterEqual(rw, view.getYmin()))) { // start labels at the y-axis instead of screen border // be careful: view.axisCross[1] = x value for which the y-axis // crosses, // so xmin is replaced view.axisCross[1] and not // view.axisCross[0] rw = MyMath.nextMultiple(view.axisCross[0], view.axesNumberingDistances[1]); smallTickOffset = axesStep; labelno = Math.round(rw / view.axesNumberingDistances[1]); } double pix = view.getYZero() - (rw * view.getYscale()); double tickStep = axesStep / 2; double maxHeight = EuclidianView .estimateNumberHeight(view.getFontAxes()); int unitsPerLabelY = (int) MyMath.nextPrettyNumber(maxHeight / axesStep, 1); if (pix > (view.getHeight() - EuclidianView.SCREEN_BORDER)) { // big tick if (drawMajorTicks[1]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(xBig, pix, xZeroTick, pix); } pix -= axesStep; rw += view.axesNumberingDistances[1]; labelno++; } double smallTickPix = pix + tickStep; // draw all of the remaining ticks and labels // int maxY = height - view.SCREEN_BORDER; int maxY = EuclidianView.SCREEN_BORDER; ArrayList<TickNumber> numbers = new ArrayList<TickNumber>(); // yAxisEnd String crossAtStr = "" + view.kernel.formatPiE(view.axisCross[0], view.axesNumberFormat[1], StringTemplate.defaultTemplate); for (; pix >= maxY; rw += view.axesNumberingDistances[1], pix -= axesStep, labelno++) { if (pix >= maxY && pix < yAxisEnd + 1) { if (view.showAxesNumbers[1]) { String strNum = tickDescription(view, labelno, 1); if ((labelno % unitsPerLabelY) == 0) { StringBuilder sb = new StringBuilder(strNum); // don't check rw < 0 as it fails for eg // -0.0000000001 if (sb.charAt(0) == '-') { // change minus sign (too short) to n-dash sb.setCharAt(0, minusSign); } if ((view.axesUnitLabels[1] != null) && !view.piAxisUnit[1]) { sb.append(view.axesUnitLabels[1]); } GTextLayout layout = AwtFactory.getPrototype() .newTextLayout(sb.toString(), view.getFontAxes(), g2.getFontRenderContext()); double width = layout.getAdvance(); int x = (int) ((xCrossPix + xoffset) - width); int y; // flag for handling label at axis cross point boolean zero = strNum.equals(crossAtStr); // if the label is at the axis cross point then draw // it 2 pixels above if (zero && view.showAxes[0] && !view.positiveAxes[0]) { y = (int) (yCrossPix - 2); } else { y = (int) (pix + yoffset); } if (view.getApplication() .has(Feature.TICK_NUMBERS_AT_EDGE)) { numbers.add(new TickNumber(g2, sb.toString(), x, y, xCrossPix, xoffset, width)); } else { // draw number drawString(g2, sb.toString(), x, y); } // measure width, so grid line can avoid it // use same (max) for all labels if (sb.charAt(0) == minusSign && width > view.yLabelMaxWidthNeg) { view.yLabelMaxWidthNeg = width; } else if (sb.charAt(0) != minusSign && width > view.yLabelMaxWidthPos) { view.yLabelMaxWidthPos = width; } // store position of number, so grid line can avoid // it view.axesLabelsPositionsY.add(Integer .valueOf((int) (pix + Kernel.MIN_PRECISION))); } } if (drawMajorTicks[1] && (!view.showAxes[0] || !Kernel.isEqual(rw, view.axisCross[0]))) { g2.setStroke(view.tickStroke); g2.drawStraightLine(xBig, pix, xZeroTick, pix); } } else if (drawMajorTicks[1] && !drawTopArrow) { // draw last tick if there is no arrow g2.setStroke(view.tickStroke); g2.drawStraightLine(xBig, pix, xZeroTick, pix); } // small tick smallTickPix = (pix + tickStep) - smallTickOffset; if (drawMinorTicks[1]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(xSmall1, smallTickPix, xSmall2, smallTickPix); } } if (view.getApplication().has(Feature.TICK_NUMBERS_AT_EDGE)) { for (int i = 0; i < numbers.size(); i++) { numbers.get(i).draw(); } } } private class TickNumber { String text; int x; int y; double xCrossPix; double xoffset; double width; GGraphics2D g2; TickNumber(GGraphics2D graphics, String text1, int x1, int y1, double xCrossPix1, double xoffset1, double width1) { text = text1; x = x1; y = y1; xCrossPix = xCrossPix1; xoffset = xoffset1; width = width1; g2 = graphics; } public void draw() { // At the left and right edge numbers will be stayed // at // the border double leftLimit = (view.yLabelMaxWidthNeg > 0 ? view.yLabelMaxWidthNeg : view.yLabelMaxWidthPos) + 10; if (xCrossPix < leftLimit) { x = (int) ((leftLimit + xoffset) - width); } else if (xCrossPix > view.getWidth()) { x = (int) (view.getWidth() - width + xoffset); } drawString(g2, text, x, y); } } private void drawYticksLog(GGraphics2D g2, double xCrossPix, int fontsize, char minusSign, boolean drawTopArrow, double yCrossPix, double yAxisEnd) { double xoffset = -4 - (fontsize / 4); double yoffset = (fontsize / 2) - 1; boolean[] drawMajorTicks = { view.axesTickStyles[0] <= 1, view.axesTickStyles[1] <= 1 }; boolean[] drawMinorTicks = { view.axesTickStyles[0] == 0, view.axesTickStyles[1] == 0 }; double xSmall1 = xCrossPix - 0; double xSmall2 = xCrossPix - 2; double xBig = xCrossPix - 3; double smallTickOffset = 0; double xZeroTick = xCrossPix; if (view.areAxesBold()) { xSmall2--; } // numbers double rw = view.getYmin() - (view.getYmin() % view.axesNumberingDistances[1]); long labelno = Math.round(rw / view.axesNumberingDistances[1]); // by default we start with minor tick to the left of first major // tick, exception is for positive only double pow = MyMath.nextPrettyNumber(view.getYmin(), 1); double axisStep = view.getHeight() / (Math.log10(view.getYmax()) - Math.log10(view.getYmin())); double pix = (Math.log10(view.getYmax()) - Math.log10(pow)) * axisStep; if (view.getPositiveAxes()[1] && (Kernel.isGreaterEqual(rw, view.getYmin()))) { // start labels at the y-axis instead of screen border // be careful: view.axisCross[1] = x value for which the y-axis // crosses, // so xmin is replaced view.axisCross[1] and not // view.axisCross[0] rw = MyMath.nextMultiple(view.axisCross[0], view.axesNumberingDistances[1]); smallTickOffset = axisStep; labelno = Math.round(rw / view.axesNumberingDistances[1]); } double tickStep = axisStep / 2; double maxHeight = EuclidianView .estimateNumberHeight(view.getFontAxes()); int unitsPerLabelY = (int) MyMath.nextPrettyNumber(maxHeight / axisStep, 1); if (pix > (view.getHeight() - EuclidianView.SCREEN_BORDER)) { // big tick if (drawMajorTicks[1]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(xBig, pix, xZeroTick, pix); } pix -= axisStep; rw += view.axesNumberingDistances[1]; labelno++; } double smallTickPix = pix + tickStep; // draw all of the remaining ticks and labels // int maxY = height - view.SCREEN_BORDER; int maxY = EuclidianView.SCREEN_BORDER; // yAxisEnd String crossAtStr = "" + view.kernel.formatPiE(view.axisCross[0], view.axesNumberFormat[1], StringTemplate.defaultTemplate); for (; pix >= maxY; rw += view.axesNumberingDistances[1], pix -= axisStep, labelno++) { if (pix >= maxY && pix < yAxisEnd + 1) { if (view.showAxesNumbers[1]) { String strNum = tickDescriptionLog(view, pow, 1); pow = pow * 10; if ((labelno % unitsPerLabelY) == 0) { StringBuilder sb = new StringBuilder(strNum); // don't check rw < 0 as it fails for eg // -0.0000000001 if (sb.charAt(0) == '-') { // change minus sign (too short) to n-dash sb.setCharAt(0, minusSign); } if ((view.axesUnitLabels[1] != null) && !view.piAxisUnit[1]) { sb.append(view.axesUnitLabels[1]); } GTextLayout layout = AwtFactory.getPrototype() .newTextLayout(sb.toString(), view.getFontAxes(), g2.getFontRenderContext()); double width = layout.getAdvance(); int x = (int) ((xCrossPix + xoffset) - width); int y; // flag for handling label at axis cross point boolean zero = strNum.equals(crossAtStr); // if the label is at the axis cross point then draw // it 2 pixels above if (zero && view.showAxes[0] && !view.positiveAxes[0]) { y = (int) (yCrossPix - 2); } else { y = (int) (pix + yoffset); } // draw number drawString(g2, sb.toString(), x, y); // measure width, so grid line can avoid it // use same (max) for all labels if (sb.charAt(0) == minusSign && width > view.yLabelMaxWidthNeg) { view.yLabelMaxWidthNeg = width; } else if (sb.charAt(0) != minusSign && width > view.yLabelMaxWidthPos) { view.yLabelMaxWidthPos = width; } // store position of number, so grid line can avoid // it view.axesLabelsPositionsY.add(Integer .valueOf((int) (pix + Kernel.MIN_PRECISION))); } } if (drawMajorTicks[1] && (!view.showAxes[0] || !Kernel.isEqual(rw, view.axisCross[0]))) { g2.setStroke(view.tickStroke); g2.drawStraightLine(xBig, pix, xZeroTick, pix); } } else if (drawMajorTicks[1] && !drawTopArrow) { // draw last tick if there is no arrow g2.setStroke(view.tickStroke); g2.drawStraightLine(xBig, pix, xZeroTick, pix); } // small tick smallTickPix = (pix + tickStep) - smallTickOffset; if (drawMinorTicks[1]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(xSmall1, smallTickPix, xSmall2, smallTickPix); } } } /* * spaceToLeft so that minus signs are more visible next to grid */ void drawString(GGraphics2D g2, String text, double x, double y) { g2.setColor(view.axesColor); g2.drawString(text, (int) (x), (int) y); } private void drawXTicksLinear(GGraphics2D g2, double yCrossPix, char minusSign, boolean drawRightArrow, int fontsize, double xAxisStart) { double yoffset = view.getYOffsetForXAxis(fontsize); boolean[] drawMajorTicks = { view.axesTickStyles[0] <= 1, view.axesTickStyles[1] <= 1 }; boolean[] drawMinorTicks = { view.axesTickStyles[0] == 0, view.axesTickStyles[1] == 0 }; double rw = view.getXmin() - (view.getXmin() % view.axesNumberingDistances[0]); long labelno = Math.round(rw / view.axesNumberingDistances[0]); // by default we start with minor tick to the left of first major // tick, exception is for positive only double smallTickOffset = 0; double axesStep = view.getXscale() * view.axesNumberingDistances[0]; // pixelstep if (view.getPositiveAxes()[0] && (Kernel.isGreaterEqual(rw, view.getXmin()))) { // start labels at the y-axis instead of screen border // be careful: view.axisCross[1] = x value for which the y-axis // crosses, // so xmin is replaced view.axisCross[1] and not // view.axisCross[0] rw = MyMath.nextMultiple(view.axisCross[1], view.axesNumberingDistances[0]); smallTickOffset = axesStep; labelno = Math.round(rw / view.axesNumberingDistances[0]); } int maxX = view.getWidth() - EuclidianView.SCREEN_BORDER; double pix = view.getXZero() + (rw * view.getXscale()); double smallTickPix; double tickStep = axesStep / 2; double labelLengthMax = Math.max( view.estimateNumberWidth(rw, view.getFontAxes()), view.estimateNumberWidth( MyMath.nextMultiple(view.getXmax(), view.axesNumberingDistances[0]), view.getFontAxes())); int unitsPerLabelX = (int) MyMath .nextPrettyNumber(labelLengthMax / axesStep, 1); String crossAtStr = "" + view.kernel.formatPiE(view.axisCross[1], view.axesNumberFormat[0], StringTemplate.defaultTemplate); double yZeroTick = yCrossPix; double yBig = yCrossPix + 3; double ySmall1 = yCrossPix + 0; double ySmall2 = yCrossPix + 2; if (view.areAxesBold()) { ySmall2++; } if (pix < EuclidianView.SCREEN_BORDER) { // big tick if (drawMajorTicks[0]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(pix, yZeroTick, pix, yBig); } pix += axesStep; labelno += 1; } for (; pix < view.getWidth(); pix += axesStep) { // 285, 285.1, 285.2 -> rounding problems if (pix >= xAxisStart && pix <= maxX) { if (view.showAxesNumbers[0]) { String strNum = tickDescription(view, labelno, 0); if ((labelno % unitsPerLabelX) == 0) { StringBuilder sb = new StringBuilder(strNum); // don't check rw < 0 as it fails for eg // -0.0000000001 if (sb.charAt(0) == '-') { // change minus sign (too short) to n-dash sb.setCharAt(0, minusSign); } if ((view.axesUnitLabels[0] != null) && !view.piAxisUnit[0]) { sb.append(view.axesUnitLabels[0]); } int x, y = (int) (yCrossPix + yoffset); // flag to handle drawing a label at axis crossing // point boolean zero = strNum.equals(crossAtStr); // if label intersects the y-axis then draw it 6 // pixels to the left if (zero && view.showAxes[1] && !view.positiveAxes[1]) { x = (int) (pix + 6); } else { x = (int) ((pix + 1) - (EuclidianView.estimateTextWidth( sb.toString(), view.getFontAxes()) / 2)); } if (view.getApplication() .has(Feature.TICK_NUMBERS_AT_EDGE)) { if (yCrossPix >= view.getHeight() - (view.xLabelHeights + 5)) { y = (int) (view.getHeight() - view.xLabelHeights - 5 + yoffset); } else if (yCrossPix <= 0) { y = (int) yoffset; } } drawString(g2, sb.toString(), x, y); // store position of number, so grid line can avoid // it view.axesLabelsPositionsX.add(Integer .valueOf((int) (pix + Kernel.MIN_PRECISION))); } } // big tick if (drawMajorTicks[0] && (!view.showAxes[1] || !Kernel.isEqual(pix, view.toScreenCoordX(view.axisCross[1])))) { g2.setStroke(view.tickStroke); g2.drawStraightLine(pix, yZeroTick, pix, yBig); } } else if (drawMajorTicks[0] && !drawRightArrow) { // draw last tick if there is no arrow g2.drawStraightLine(pix, yZeroTick, pix, yBig); } // small tick smallTickPix = (pix - tickStep) + smallTickOffset; if (drawMinorTicks[0]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(smallTickPix, ySmall1, smallTickPix, ySmall2); } labelno++; } // last small tick smallTickPix = (pix - tickStep) + smallTickOffset; if (drawMinorTicks[0]) { g2.drawStraightLine(smallTickPix, ySmall1, smallTickPix, ySmall2); } } private void drawXTicksLog(GGraphics2D g2, double yCrossPix, char minusSign, boolean drawRightArrow, int fontsize, double xAxisStart) { double yoffset = view.getYOffsetForXAxis(fontsize); boolean[] drawMajorTicks = { view.axesTickStyles[0] <= 1, view.axesTickStyles[1] <= 1 }; boolean[] drawMinorTicks = { view.axesTickStyles[0] == 0, view.axesTickStyles[1] == 0 }; // by default we start with minor tick to the left of first major // tick, exception is for positive only double smallTickOffset = 0; int maxX = view.getWidth() - EuclidianView.SCREEN_BORDER; double smallTickPix; // TODO use only pretty numbers when zoomed String crossAtStr = "" + view.kernel.formatPiE(view.axisCross[1], view.axesNumberFormat[0], StringTemplate.defaultTemplate); double yZeroTick = yCrossPix; double yBig = yCrossPix + 3; double ySmall1 = yCrossPix + 0; double ySmall2 = yCrossPix + 2; if (view.areAxesBold()) { ySmall2++; } double pow = MyMath.nextPrettyNumber(view.getXmin(), 1); double axisStep = view.getWidth() / (Math.log10(view.getXmax()) - Math.log10(view.getXmin())); double pix = (Math.log10(pow) - Math.log10(view.getXmin())) * axisStep; while (pow < view.getXmax()) { // 285, 285.1, 285.2 -> rounding problems if (pix >= xAxisStart && pix <= maxX) { if (view.showAxesNumbers[0]) { String strNum = tickDescriptionLog(view, pow, 0); StringBuilder sb = new StringBuilder(strNum); // don't check rw < 0 as it fails for eg // -0.0000000001 if (sb.charAt(0) == '-') { // change minus sign (too short) to n-dash sb.setCharAt(0, minusSign); } if ((view.axesUnitLabels[0] != null) && !view.piAxisUnit[0]) { sb.append(view.axesUnitLabels[0]); } int x, y = (int) (yCrossPix + yoffset); // flag to handle drawing a label at axis crossing // point boolean zero = strNum.equals(crossAtStr); // if label intersects the y-axis then draw it 6 // pixels to the left if (zero && view.showAxes[1] && !view.positiveAxes[1]) { x = (int) (pix + 6); } else { x = (int) ((pix + 1) - (EuclidianView.estimateTextWidth( sb.toString(), view.getFontAxes()) / 2)); } drawString(g2, sb.toString(), x, y); // store position of number, so grid line can avoid // it view.axesLabelsPositionsX.add(Integer .valueOf((int) (pix + Kernel.MIN_PRECISION))); } // big tick if (drawMajorTicks[0] && (!view.showAxes[1] || !Kernel.isEqual(pix, view.toScreenCoordX(view.axisCross[1])))) { g2.setStroke(view.tickStroke); g2.drawStraightLine(pix, yZeroTick, pix, yBig); } } else if (drawMajorTicks[0] && !drawRightArrow) { // draw last tick if there is no arrow g2.drawStraightLine(pix, yZeroTick, pix, yBig); } // small tick smallTickPix = (pix - 0) + smallTickOffset; if (drawMinorTicks[0]) { g2.setStroke(view.tickStroke); g2.drawStraightLine(smallTickPix, ySmall1, smallTickPix, ySmall2); } pow = pow * 10; pix += axisStep; } // last small tick smallTickPix = (pix - 0) + smallTickOffset; if (drawMinorTicks[0]) { g2.drawStraightLine(smallTickPix, ySmall1, smallTickPix, ySmall2); } } /** * @param view * view * @param labelno * coefficient * @param axis * axis index * @return description */ public static String tickDescription(EuclidianView view, long labelno, int axis) { if (view.getAxesDistanceObjects()[axis] != null && !view.isAutomaticAxesNumberingDistance()[axis] && view.getAxesDistanceObjects()[axis].getDefinition() != null && view.getAxesDistanceObjects()[axis].getDouble() > 0) { return multiple(view.getAxesDistanceObjects()[axis].getDefinition(), labelno); } return view.kernel.formatPiE( Kernel.checkDecimalFraction( labelno * view.axesNumberingDistances[axis]), view.axesNumberFormat[axis], StringTemplate.defaultTemplate); } /** * @param view * view * @param num * numbr to be printed * @param axis * 0 for x,1 for y * @return description */ public static String tickDescriptionLog(EuclidianView view, double num, int axis) { return view.kernel.formatPiE(Kernel.checkDecimalFraction(num), view.axesNumberFormat[axis], StringTemplate.defaultTemplate); } /** * @param definition * step definition * @param labelno * step coefficient * @return description of labelno*definition */ public static String multiple(ExpressionNode definition, long labelno) { return definition.multiply(labelno) .toFractionString(StringTemplate.defaultTemplate); } }