X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.scenegraph%2Fsrc%2Forg%2Fsimantics%2Fscenegraph%2Fg2d%2Fnodes%2FFlagNode.java;h=b2e973949517d7e66ff06391c7e1b20e27bd90fc;hb=fe29fd8956c3881e261ec4eee1cdd2ac27bc0554;hp=c62fc84da66af45c2afe503ebe19cebc28cc9c55;hpb=f8576d4d2b3b30d76db552d624fc9f087b8940bd;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java index c62fc84da..b2e973949 100644 --- a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java @@ -1,371 +1,375 @@ -/******************************************************************************* - * Copyright (c) 2007, 2010 Association for Decentralized Information Management - * in Industry THTH ry. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * VTT Technical Research Centre of Finland - initial API and implementation - *******************************************************************************/ -package org.simantics.scenegraph.g2d.nodes; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.Shape; -import java.awt.Stroke; -import java.awt.font.FontRenderContext; -import java.awt.font.TextLayout; -import java.awt.geom.AffineTransform; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.util.Arrays; - -import org.simantics.scenegraph.g2d.G2DNode; -import org.simantics.scenegraph.g2d.G2DPDFRenderingHints; -import org.simantics.scenegraph.utils.GeometryUtils; - -public class FlagNode extends G2DNode { - - private static final long serialVersionUID = -1716729504104107151L; - - private static final AffineTransform IDENTITY = new AffineTransform(); - - private static final byte LEADING = 0; - private static final byte TRAILING = 1; - private static final byte CENTER = 2; - - private static final boolean DEBUG = false; - - private static final double GLOBAL_SCALE = 0.1; - - private static final double TEXT_MARGIN = 5; - - static transient final BasicStroke STROKE = new BasicStroke(0.25f, BasicStroke.CAP_BUTT, - BasicStroke.JOIN_MITER); - - final transient Font FONT = Font.decode("Arial 12"); - - protected boolean visible; - - protected Shape flagShape; - protected String[] flagText; - protected Stroke stroke; - protected Color border; - protected Color fill; - protected Color textColor; - protected float width; - protected float height; - protected double direction; // in radians - protected float beakAngle; - protected Rectangle2D textArea; - protected byte hAlign; - protected byte vAlign; - - private transient final Point2D origin = new Point2D.Double(); - private transient final Point2D xa = new Point2D.Double(); - private transient final Point2D ya = new Point2D.Double(); - - protected transient TextLayout[] textLayout = null; - protected transient Rectangle2D[] rects = null; - protected transient float textHeight = 0; - protected transient float lastViewScale = 0; - - @SyncField("visible") - public void setVisible(boolean visible) { - this.visible = visible; - } - - public boolean isVisible() { - return visible; - } - - @SyncField({"visible", "flagShape", "flagText", "stroke", "border", "fill", "textColor", "width", "height", "direction", "beakAngle", "textSize", "hAlign", "vAlign"}) - public void init(Shape flagShape, String[] flagText, Stroke stroke, Color border, Color fill, Color textColor, float width, float height, double direction, float beakAngle, Rectangle2D textArea, int hAlign, int vAlign) { - this.visible = true; - this.flagShape = flagShape; - this.flagText = flagText; - this.stroke = stroke; - this.border = border; - this.fill = fill; - this.textColor = textColor; - this.width = width; - this.height = height; - this.direction = direction; - this.beakAngle = beakAngle; - this.textArea = textArea; - this.hAlign = (byte) hAlign; - this.vAlign = (byte) vAlign; - - resetCaches(); - } - - private void resetCaches() { - textLayout = null; - rects = null; - } - - @Override - public void render(Graphics2D g) { - if (!visible) - return; - - if (DEBUG) { - System.out.println("FlagNode.render:"); - System.out.println("\tflagShape: " + flagShape); - System.out.println("\tflagText: " + Arrays.toString(flagText)); - System.out.println("\tstroke: " + stroke); - System.out.println("\tborder: " + border); - System.out.println("\tfill: " + fill); - System.out.println("\ttextColor: " + textColor); - System.out.println("\twidth: " + width); - System.out.println("\theight: " + height); - System.out.println("\tdirection: " + direction); - System.out.println("\tbeakAngle: " + beakAngle); - System.out.println("\ttextArea: " + textArea); - System.out.println("\thAlign: " + hAlign); - System.out.println("\tvAlign: " + vAlign); - System.out.println("\tdraw: " + visible); - } - - AffineTransform ot = g.getTransform(); - g.transform(transform); - - try { - Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING); - - //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // Paint flag shape - g.setColor(fill); - g.fill(flagShape); - g.setStroke(stroke); - g.setColor(border); - g.draw(flagShape); - - // Speed rendering optimization: don't draw text that is too small to read - if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) { - double viewScale = GeometryUtils.getScale(ot); - viewScale *= GeometryUtils.getScale(transform); - if (viewScale < 4.0) - return; - } - - if (flagText == null || flagText.length == 0) - return; - - if (DEBUG) { - g.setColor(Color.RED); - g.draw(textArea); - } - - // Paint flag text - Font f = FONT; - g.setFont(f); - g.setColor(textColor); - - AffineTransform orig = g.getTransform(); - - double det = orig.getDeterminant(); - if (DEBUG) - System.out.println("DETERMINANT: " + det); - - if (det < 0) { - // Invert the Y-axis if the symbol is "flipped" either vertically xor horizontally - origin.setLocation(textArea.getMinX(), textArea.getMaxY()); - xa.setLocation(textArea.getMaxX(), textArea.getMaxY()); - ya.setLocation(textArea.getMinX(), textArea.getMinY()); - } else { - origin.setLocation(textArea.getMinX(), textArea.getMinY()); - xa.setLocation(textArea.getMaxX(), textArea.getMinY()); - ya.setLocation(textArea.getMinX(), textArea.getMaxY()); - } - - orig.transform(origin, origin); - orig.transform(xa, xa); - orig.transform(ya, ya); - - double xAxisX = xa.getX() - origin.getX(); - double xAxisY = xa.getY() - origin.getY(); - double yAxisX = ya.getX() - origin.getX(); - double yAxisY = ya.getY() - origin.getY(); - - boolean needToFlip = xAxisX < 0 || yAxisY < 0; - if (DEBUG) - System.out.println("TEXT NEEDS FLIPPING: " + needToFlip); - - byte horizAlign = hAlign; - - if (needToFlip) { - // Okay, the text would be upside-down if rendered directly with these axes. - // Let's flip the origin to the diagonal point and - // invert both x & y axis of the text area to get - // the text the right way around. Also, horizontal alignment - // needs to be switched unless it's centered. - origin.setLocation(origin.getX() + xAxisX + yAxisX, origin.getY() + xAxisY + yAxisY); - xAxisX = -xAxisX; - xAxisY = -xAxisY; - yAxisX = -yAxisX; - yAxisY = -yAxisY; - - // Must flip horizontal alignment to keep text visually at the same - // end as before. - if (horizAlign == LEADING) - horizAlign = TRAILING; - else if (horizAlign == TRAILING) - horizAlign = LEADING; - } - - final double gScale = GLOBAL_SCALE; - final double gScaleRecip = 1.0 / gScale; - final double scale = GeometryUtils.getMaxScale(orig) * gScale; - final double rotation = Math.atan2(xAxisY, xAxisX); - g.setTransform(IDENTITY); - g.translate(origin.getX(), origin.getY()); - g.rotate(rotation); - g.scale(scale, scale); - - if (DEBUG) { - System.out.println("ORIGIN: " + origin); - System.out.println("X-AXIS: (" + xAxisX + "," + xAxisY + ")"); - System.out.println("Y-AXIS: (" + yAxisX + "," + yAxisY + ")"); - System.out.println("rotation: " + Math.toDegrees(rotation)); - System.out.println("scale: " + scale); - System.out.println("ORIG transform: " + orig); - System.out.println("transform: " + g.getTransform()); - } - - FontMetrics fm = g.getFontMetrics(f); - double fontHeight = fm.getHeight(); - - if (textLayout == null || (float) scale != lastViewScale) - { - lastViewScale = (float) scale; - FontRenderContext frc = g.getFontRenderContext(); - if (textLayout == null) - textLayout = new TextLayout[flagText.length]; - if (rects == null) - rects = new Rectangle2D[flagText.length]; - textHeight = 0; - for (int i = 0; i < flagText.length; ++i) { - String txt = flagText[i].isEmpty() ? " " : flagText[i]; - textLayout[i] = new TextLayout(txt, f, frc); - rects[i] = textLayout[i].getBounds(); - - // If the bb height is not overridden with the font height - // text lines will not be drawn in the correct Y location. - rects[i].setRect(rects[i].getX(), rects[i].getY(), rects[i].getWidth(), fontHeight); - - textHeight += rects[i].getHeight() * gScale; - if (DEBUG) - System.out.println(" bounding rectangle for line " + i + " '" + flagText[i] + "': " + rects[i]); - } - } - - double leftoverHeight = textArea.getHeight() - textHeight; - if (leftoverHeight < 0) - leftoverHeight = 0; - - if (DEBUG) { - System.out.println("text area height: " + textArea.getHeight()); - System.out.println("total text height: " + textHeight); - System.out.println("leftover height: " + leftoverHeight); - } - - double lineDist = 0; - double startY = 0; - - switch (vAlign) { - case LEADING: - if (DEBUG) - System.out.println("VERTICAL LEADING"); - lineDist = leftoverHeight / flagText.length; - startY = fm.getMaxAscent(); - break; - case TRAILING: - if (DEBUG) - System.out.println("VERTICAL TRAILING"); - lineDist = leftoverHeight / flagText.length; - startY = fm.getMaxAscent() + lineDist * gScaleRecip; - break; - case CENTER: - if (DEBUG) - System.out.println("VERTICAL CENTER"); - lineDist = leftoverHeight / (flagText.length + 1); - startY = fm.getMaxAscent() + lineDist * gScaleRecip; - break; - } - - if (DEBUG) { - System.out.println("lineDist: " + lineDist); - System.out.println("startY: " + startY); - } - - lineDist *= gScaleRecip; - double y = startY; - double textAreaWidth = textArea.getWidth() * gScaleRecip; - boolean isRenderingPdf = g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER) != null; - - for (int i = 0; i < flagText.length; ++i) { - //String line = flagText[i]; - Rectangle2D rect = rects[i]; - - double x = 0; - - switch (horizAlign) { - case LEADING: - if (DEBUG) - System.out.println("HORIZ LEADING: " + rect); - x = TEXT_MARGIN; - break; - case TRAILING: - if (DEBUG) - System.out.println("HORIZ TRAILING: " + rect); - x = textAreaWidth - rect.getWidth() - TEXT_MARGIN;; - break; - case CENTER: - if (DEBUG) - System.out.println("HORIZ CENTER: " + rect); - x = textAreaWidth * 0.5 - rect.getWidth()*0.5; - break; - } - - if (DEBUG) - System.out.println(" X, Y: " + x + ", " + y); - - if (DEBUG) - System.out.println(" DRAW: '" + flagText[i] + "' with " + g.getTransform()); - - // #6459: render as text in PDF and paths on screen - if (isRenderingPdf) - g.drawString(flagText[i], (float) x, (float) y); - else - textLayout[i].draw(g, (float) x, (float) y); - - y += lineDist; - y += rect.getHeight(); - } - - } finally { - g.setTransform(ot); - } - } - - public static double getBeakLength(double height, double beakAngle) { - beakAngle = Math.min(180, Math.max(10, beakAngle)); - return height / (2*Math.tan(Math.toRadians(beakAngle) / 2)); - } - - @Override - public Rectangle2D getBoundsInLocal() { - if (flagShape == null) - return null; - return flagShape.getBounds2D(); - } - -} +/******************************************************************************* + * Copyright (c) 2007, 2010 Association for Decentralized Information Management + * in Industry THTH ry. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VTT Technical Research Centre of Finland - initial API and implementation + *******************************************************************************/ +package org.simantics.scenegraph.g2d.nodes; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.font.FontRenderContext; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.Arrays; + +import org.simantics.scenegraph.g2d.G2DNode; +import org.simantics.scenegraph.g2d.G2DPDFRenderingHints; +import org.simantics.scenegraph.g2d.G2DRenderingHints; +import org.simantics.scenegraph.g2d.G2DRenderingHints.TextRenderingMode; +import org.simantics.scenegraph.utils.GeometryUtils; + +public class FlagNode extends G2DNode { + + private static final long serialVersionUID = -1716729504104107151L; + + private static final AffineTransform IDENTITY = new AffineTransform(); + + private static final byte LEADING = 0; + private static final byte TRAILING = 1; + private static final byte CENTER = 2; + + private static final boolean DEBUG = false; + + private static final double GLOBAL_SCALE = 0.1; + + private static final double TEXT_MARGIN = 5; + + static transient final BasicStroke STROKE = new BasicStroke(0.25f, BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER); + + public final static Font DEFAULT_FONT = Font.decode("Arial 12"); + + protected boolean visible; + + protected Shape flagShape; + protected String[] flagText; + protected Stroke stroke; + protected Color border; + protected Color fill; + protected Color textColor; + protected float width; + protected float height; + protected double direction; // in radians + protected float beakAngle; + protected Rectangle2D textArea; + protected byte hAlign; + protected byte vAlign; + protected Font font = DEFAULT_FONT; + + private transient final Point2D origin = new Point2D.Double(); + private transient final Point2D xa = new Point2D.Double(); + private transient final Point2D ya = new Point2D.Double(); + + protected transient TextLayout[] textLayout = null; + protected transient Rectangle2D[] rects = null; + protected transient float textHeight = 0; + protected transient float lastViewScale = 0; + + @SyncField("visible") + public void setVisible(boolean visible) { + this.visible = visible; + } + + public boolean isVisible() { + return visible; + } + + @SyncField({"visible", "flagShape", "flagText", "stroke", "border", "fill", "textColor", "width", "height", "direction", "beakAngle", "textSize", "hAlign", "vAlign", "font"}) + public void init(Shape flagShape, String[] flagText, Stroke stroke, Color border, Color fill, Color textColor, float width, float height, double direction, float beakAngle, Rectangle2D textArea, int hAlign, int vAlign, Font font) { + this.visible = true; + this.flagShape = flagShape; + this.flagText = flagText; + this.stroke = stroke; + this.border = border; + this.fill = fill; + this.textColor = textColor; + this.width = width; + this.height = height; + this.direction = direction; + this.beakAngle = beakAngle; + this.textArea = textArea; + this.hAlign = (byte) hAlign; + this.vAlign = (byte) vAlign; + this.font = font; + + resetCaches(); + } + + private void resetCaches() { + textLayout = null; + rects = null; + } + + @Override + public void render(Graphics2D g) { + if (!visible) + return; + + if (DEBUG) { + System.out.println("FlagNode.render:"); + System.out.println("\tflagShape: " + flagShape); + System.out.println("\tflagText: " + Arrays.toString(flagText)); + System.out.println("\tstroke: " + stroke); + System.out.println("\tborder: " + border); + System.out.println("\tfill: " + fill); + System.out.println("\ttextColor: " + textColor); + System.out.println("\twidth: " + width); + System.out.println("\theight: " + height); + System.out.println("\tdirection: " + direction); + System.out.println("\tbeakAngle: " + beakAngle); + System.out.println("\ttextArea: " + textArea); + System.out.println("\thAlign: " + hAlign); + System.out.println("\tvAlign: " + vAlign); + System.out.println("\tdraw: " + visible); + } + + AffineTransform ot = g.getTransform(); + g.transform(transform); + + try { + Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING); + + //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Paint flag shape + g.setColor(fill); + g.fill(flagShape); + g.setStroke(stroke); + g.setColor(border); + g.draw(flagShape); + + // Speed rendering optimization: don't draw text that is too small to read + if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) { + double viewScale = GeometryUtils.getScale(ot); + viewScale *= GeometryUtils.getScale(transform); + if (viewScale < 4.0) + return; + } + + if (flagText == null || flagText.length == 0) + return; + + if (DEBUG) { + g.setColor(Color.RED); + g.draw(textArea); + } + + // Paint flag text + g.setFont(font); + g.setColor(textColor); + + AffineTransform orig = g.getTransform(); + + double det = orig.getDeterminant(); + if (DEBUG) + System.out.println("DETERMINANT: " + det); + + if (det < 0) { + // Invert the Y-axis if the symbol is "flipped" either vertically xor horizontally + origin.setLocation(textArea.getMinX(), textArea.getMaxY()); + xa.setLocation(textArea.getMaxX(), textArea.getMaxY()); + ya.setLocation(textArea.getMinX(), textArea.getMinY()); + } else { + origin.setLocation(textArea.getMinX(), textArea.getMinY()); + xa.setLocation(textArea.getMaxX(), textArea.getMinY()); + ya.setLocation(textArea.getMinX(), textArea.getMaxY()); + } + + orig.transform(origin, origin); + orig.transform(xa, xa); + orig.transform(ya, ya); + + double xAxisX = xa.getX() - origin.getX(); + double xAxisY = xa.getY() - origin.getY(); + double yAxisX = ya.getX() - origin.getX(); + double yAxisY = ya.getY() - origin.getY(); + + boolean needToFlip = xAxisX < 0 || yAxisY < 0; + if (DEBUG) + System.out.println("TEXT NEEDS FLIPPING: " + needToFlip); + + byte horizAlign = hAlign; + + if (needToFlip) { + // Okay, the text would be upside-down if rendered directly with these axes. + // Let's flip the origin to the diagonal point and + // invert both x & y axis of the text area to get + // the text the right way around. Also, horizontal alignment + // needs to be switched unless it's centered. + origin.setLocation(origin.getX() + xAxisX + yAxisX, origin.getY() + xAxisY + yAxisY); + xAxisX = -xAxisX; + xAxisY = -xAxisY; + yAxisX = -yAxisX; + yAxisY = -yAxisY; + + // Must flip horizontal alignment to keep text visually at the same + // end as before. + if (horizAlign == LEADING) + horizAlign = TRAILING; + else if (horizAlign == TRAILING) + horizAlign = LEADING; + } + + final double gScale = GLOBAL_SCALE; + final double gScaleRecip = 1.0 / gScale; + final double scale = GeometryUtils.getMaxScale(orig) * gScale; + final double rotation = Math.atan2(xAxisY, xAxisX); + g.setTransform(IDENTITY); + g.translate(origin.getX(), origin.getY()); + g.rotate(rotation); + g.scale(scale, scale); + + if (DEBUG) { + System.out.println("ORIGIN: " + origin); + System.out.println("X-AXIS: (" + xAxisX + "," + xAxisY + ")"); + System.out.println("Y-AXIS: (" + yAxisX + "," + yAxisY + ")"); + System.out.println("rotation: " + Math.toDegrees(rotation)); + System.out.println("scale: " + scale); + System.out.println("ORIG transform: " + orig); + System.out.println("transform: " + g.getTransform()); + } + + FontMetrics fm = g.getFontMetrics(font); + double fontHeight = fm.getHeight(); + + if (textLayout == null || (float) scale != lastViewScale) + { + lastViewScale = (float) scale; + FontRenderContext frc = g.getFontRenderContext(); + if (textLayout == null) + textLayout = new TextLayout[flagText.length]; + if (rects == null) + rects = new Rectangle2D[flagText.length]; + textHeight = 0; + for (int i = 0; i < flagText.length; ++i) { + String txt = flagText[i].isEmpty() ? " " : flagText[i]; + textLayout[i] = new TextLayout(txt, font, frc); + rects[i] = textLayout[i].getBounds(); + + // If the bb height is not overridden with the font height + // text lines will not be drawn in the correct Y location. + rects[i].setRect(rects[i].getX(), rects[i].getY(), rects[i].getWidth(), fontHeight); + + textHeight += rects[i].getHeight() * gScale; + if (DEBUG) + System.out.println(" bounding rectangle for line " + i + " '" + flagText[i] + "': " + rects[i]); + } + } + + double leftoverHeight = textArea.getHeight() - textHeight; + if (leftoverHeight < 0) + leftoverHeight = 0; + + if (DEBUG) { + System.out.println("text area height: " + textArea.getHeight()); + System.out.println("total text height: " + textHeight); + System.out.println("leftover height: " + leftoverHeight); + } + + double lineDist = 0; + double startY = 0; + + switch (vAlign) { + case LEADING: + if (DEBUG) + System.out.println("VERTICAL LEADING"); + lineDist = leftoverHeight / flagText.length; + startY = fm.getMaxAscent(); + break; + case TRAILING: + if (DEBUG) + System.out.println("VERTICAL TRAILING"); + lineDist = leftoverHeight / flagText.length; + startY = fm.getMaxAscent() + lineDist * gScaleRecip; + break; + case CENTER: + if (DEBUG) + System.out.println("VERTICAL CENTER"); + lineDist = leftoverHeight / (flagText.length + 1); + startY = fm.getMaxAscent() + lineDist * gScaleRecip; + break; + } + + if (DEBUG) { + System.out.println("lineDist: " + lineDist); + System.out.println("startY: " + startY); + } + + lineDist *= gScaleRecip; + double y = startY; + double textAreaWidth = textArea.getWidth() * gScaleRecip; + boolean renderAsText = g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER) != null + || g.getRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE) == TextRenderingMode.AS_TEXT; + + for (int i = 0; i < flagText.length; ++i) { + //String line = flagText[i]; + Rectangle2D rect = rects[i]; + + double x = 0; + + switch (horizAlign) { + case LEADING: + if (DEBUG) + System.out.println("HORIZ LEADING: " + rect); + x = TEXT_MARGIN; + break; + case TRAILING: + if (DEBUG) + System.out.println("HORIZ TRAILING: " + rect); + x = textAreaWidth - rect.getWidth() - TEXT_MARGIN;; + break; + case CENTER: + if (DEBUG) + System.out.println("HORIZ CENTER: " + rect); + x = textAreaWidth * 0.5 - rect.getWidth()*0.5; + break; + } + + if (DEBUG) + System.out.println(" X, Y: " + x + ", " + y); + + if (DEBUG) + System.out.println(" DRAW: '" + flagText[i] + "' with " + g.getTransform()); + + // #6459: render as text in PDF and paths on screen + if (renderAsText) + g.drawString(flagText[i], (float) x, (float) y); + else + textLayout[i].draw(g, (float) x, (float) y); + + y += lineDist; + y += rect.getHeight(); + } + + } finally { + g.setTransform(ot); + } + } + + public static double getBeakLength(double height, double beakAngle) { + beakAngle = Math.min(180, Math.max(10, beakAngle)); + return height / (2*Math.tan(Math.toRadians(beakAngle) / 2)); + } + + @Override + public Rectangle2D getBoundsInLocal() { + if (flagShape == null) + return null; + return flagShape.getBounds2D(); + } + +}