/******************************************************************************* * 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(); } }