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