Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / FlagNode.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.scenegraph.g2d.nodes;\r
13 \r
14 import java.awt.BasicStroke;\r
15 import java.awt.Color;\r
16 import java.awt.Font;\r
17 import java.awt.FontMetrics;\r
18 import java.awt.Graphics2D;\r
19 import java.awt.RenderingHints;\r
20 import java.awt.Shape;\r
21 import java.awt.Stroke;\r
22 import java.awt.font.FontRenderContext;\r
23 import java.awt.font.TextLayout;\r
24 import java.awt.geom.AffineTransform;\r
25 import java.awt.geom.Point2D;\r
26 import java.awt.geom.Rectangle2D;\r
27 import java.util.Arrays;\r
28 \r
29 import org.simantics.scenegraph.g2d.G2DNode;\r
30 import org.simantics.scenegraph.utils.GeometryUtils;\r
31 \r
32 public class FlagNode extends G2DNode {\r
33 \r
34     private static final long          serialVersionUID = -1716729504104107151L;\r
35 \r
36     private static final AffineTransform IDENTITY         = new AffineTransform();\r
37 \r
38     private static final byte            LEADING          = 0;\r
39     private static final byte            TRAILING         = 1;\r
40     private static final byte            CENTER           = 2;\r
41 \r
42     private static final boolean         DEBUG            = false;\r
43 \r
44     private static final double          GLOBAL_SCALE     = 0.1;\r
45 \r
46     private static final double          TEXT_MARGIN      = 5;\r
47 \r
48     static transient final BasicStroke   STROKE           = new BasicStroke(0.25f, BasicStroke.CAP_BUTT,\r
49                                                                   BasicStroke.JOIN_MITER);\r
50 \r
51     final transient Font                 FONT             = Font.decode("Arial 12");\r
52 \r
53     protected boolean visible;\r
54 \r
55     protected Shape flagShape;\r
56     protected String[] flagText;\r
57     protected Stroke stroke;\r
58     protected Color border;\r
59     protected Color fill;\r
60     protected Color textColor;\r
61     protected float width;\r
62     protected float height;\r
63     protected double direction; // in radians\r
64     protected float beakAngle;\r
65     protected Rectangle2D textArea;\r
66     protected byte hAlign;\r
67     protected byte vAlign;\r
68 \r
69     private transient final Point2D      origin           = new Point2D.Double();\r
70     private transient final Point2D      xa               = new Point2D.Double();\r
71     private transient final Point2D      ya               = new Point2D.Double();\r
72 \r
73     protected transient TextLayout[]     textLayout       = null;\r
74     protected transient Rectangle2D[]    rects            = null;\r
75     protected transient float            textHeight       = 0;\r
76     protected transient float            lastViewScale    = 0;\r
77 \r
78     @SyncField("visible")\r
79     public void setVisible(boolean visible) {\r
80         this.visible = visible;\r
81     }\r
82 \r
83     public boolean isVisible() {\r
84         return visible;\r
85     }\r
86 \r
87     @SyncField({"visible", "flagShape", "flagText", "stroke", "border", "fill", "textColor", "width", "height", "direction", "beakAngle", "textSize", "hAlign", "vAlign"})\r
88     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
89         this.visible = true;\r
90         this.flagShape = flagShape;\r
91         this.flagText = flagText;\r
92         this.stroke = stroke;\r
93         this.border = border;\r
94         this.fill = fill;\r
95         this.textColor = textColor;\r
96         this.width = width;\r
97         this.height = height;\r
98         this.direction = direction;\r
99         this.beakAngle = beakAngle;\r
100         this.textArea = textArea;\r
101         this.hAlign =  (byte) hAlign;\r
102         this.vAlign = (byte) vAlign;\r
103 \r
104         resetCaches();\r
105     }\r
106 \r
107     private void resetCaches() {\r
108         textLayout = null;\r
109         rects = null;\r
110     }\r
111 \r
112     @Override\r
113     public void render(Graphics2D g) {\r
114         if (!visible)\r
115             return;\r
116 \r
117         if (DEBUG) {\r
118             System.out.println("FlagNode.render:");\r
119             System.out.println("\tflagShape:       " + flagShape);\r
120             System.out.println("\tflagText:     " + Arrays.toString(flagText));\r
121             System.out.println("\tstroke:       " + stroke);\r
122             System.out.println("\tborder:       " + border);\r
123             System.out.println("\tfill:         " + fill);\r
124             System.out.println("\ttextColor:    " + textColor);\r
125             System.out.println("\twidth:        " + width);\r
126             System.out.println("\theight:       " + height);\r
127             System.out.println("\tdirection:    " + direction);\r
128             System.out.println("\tbeakAngle:    " + beakAngle);\r
129             System.out.println("\ttextArea:     " + textArea);\r
130             System.out.println("\thAlign:       " + hAlign);\r
131             System.out.println("\tvAlign:       " + vAlign);\r
132             System.out.println("\tdraw:         " + visible);\r
133         }\r
134 \r
135         AffineTransform ot = g.getTransform();\r
136         g.transform(transform);\r
137 \r
138         try {\r
139             Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);\r
140 \r
141             //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\r
142 \r
143             // Paint flag shape\r
144             g.setColor(fill);\r
145             g.fill(flagShape);\r
146             g.setStroke(stroke);\r
147             g.setColor(border);\r
148             g.draw(flagShape);\r
149 \r
150             // Speed rendering optimization: don't draw text that is too small to read\r
151             if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {\r
152                 double viewScale = GeometryUtils.getScale(ot);\r
153                 viewScale *= GeometryUtils.getScale(transform);\r
154                 if (viewScale < 4.0)\r
155                     return;\r
156             }\r
157 \r
158             if (flagText == null || flagText.length == 0)\r
159                 return;\r
160 \r
161             if (DEBUG) {\r
162                 g.setColor(Color.RED);\r
163                 g.draw(textArea);\r
164             }\r
165 \r
166             // Paint flag text\r
167             Font f = FONT;\r
168             g.setFont(f);\r
169             g.setColor(textColor);\r
170 \r
171             AffineTransform orig = g.getTransform();\r
172 \r
173             double det = orig.getDeterminant();\r
174             if (DEBUG)\r
175                 System.out.println("DETERMINANT: " + det);\r
176 \r
177             if (det < 0) {\r
178                 // Invert the Y-axis if the symbol is "flipped" either vertically xor horizontally\r
179                 origin.setLocation(textArea.getMinX(), textArea.getMaxY());\r
180                 xa.setLocation(textArea.getMaxX(), textArea.getMaxY());\r
181                 ya.setLocation(textArea.getMinX(), textArea.getMinY());\r
182             } else {\r
183                 origin.setLocation(textArea.getMinX(), textArea.getMinY());\r
184                 xa.setLocation(textArea.getMaxX(), textArea.getMinY());\r
185                 ya.setLocation(textArea.getMinX(), textArea.getMaxY());\r
186             }\r
187 \r
188             orig.transform(origin, origin);\r
189             orig.transform(xa, xa);\r
190             orig.transform(ya, ya);\r
191 \r
192             double xAxisX = xa.getX() - origin.getX();\r
193             double xAxisY = xa.getY() - origin.getY();\r
194             double yAxisX = ya.getX() - origin.getX();\r
195             double yAxisY = ya.getY() - origin.getY();\r
196 \r
197             boolean needToFlip = xAxisX < 0 || yAxisY < 0;\r
198             if (DEBUG)\r
199                 System.out.println("TEXT NEEDS FLIPPING: " + needToFlip);\r
200 \r
201             byte horizAlign = hAlign;\r
202 \r
203             if (needToFlip) {\r
204                 // Okay, the text would be upside-down if rendered directly with these axes.\r
205                 // Let's flip the origin to the diagonal point and\r
206                 // invert both x & y axis of the text area to get\r
207                 // the text the right way around. Also, horizontal alignment\r
208                 // needs to be switched unless it's centered.\r
209                 origin.setLocation(origin.getX() + xAxisX + yAxisX, origin.getY() + xAxisY + yAxisY);\r
210                 xAxisX = -xAxisX;\r
211                 xAxisY = -xAxisY;\r
212                 yAxisX = -yAxisX;\r
213                 yAxisY = -yAxisY;\r
214 \r
215                 // Must flip horizontal alignment to keep text visually at the same\r
216                 // end as before.\r
217                 if (horizAlign == LEADING)\r
218                     horizAlign = TRAILING;\r
219                 else if (horizAlign == TRAILING)\r
220                     horizAlign = LEADING;\r
221             }\r
222 \r
223             final double gScale = GLOBAL_SCALE;\r
224             final double gScaleRecip = 1.0 / gScale;\r
225             final double scale = GeometryUtils.getMaxScale(orig) * gScale;\r
226             final double rotation = Math.atan2(xAxisY, xAxisX);\r
227             g.setTransform(IDENTITY);\r
228             g.translate(origin.getX(), origin.getY());\r
229             g.rotate(rotation);\r
230             g.scale(scale, scale);\r
231 \r
232             if (DEBUG) {\r
233                 System.out.println("ORIGIN: " + origin);\r
234                 System.out.println("X-AXIS: (" + xAxisX + "," + xAxisY + ")");\r
235                 System.out.println("Y-AXIS: (" + yAxisX + "," + yAxisY + ")");\r
236                 System.out.println("rotation: " + Math.toDegrees(rotation));\r
237                 System.out.println("scale: " + scale);\r
238                 System.out.println("ORIG transform: " + orig);\r
239                 System.out.println("transform: " + g.getTransform());\r
240             }\r
241 \r
242             FontMetrics fm = g.getFontMetrics(f);\r
243             double fontHeight = fm.getHeight();\r
244 \r
245             if (textLayout == null || (float) scale != lastViewScale)\r
246             {\r
247                 lastViewScale = (float) scale;\r
248                 FontRenderContext frc = g.getFontRenderContext();\r
249                 if (textLayout == null)\r
250                     textLayout = new TextLayout[flagText.length];\r
251                 if (rects == null)\r
252                     rects = new Rectangle2D[flagText.length];\r
253                 textHeight = 0;\r
254                 for (int i = 0; i < flagText.length; ++i) {\r
255                     String txt = flagText[i].isEmpty() ? " " : flagText[i]; \r
256                     textLayout[i] = new TextLayout(txt, f, frc);\r
257                     rects[i] = textLayout[i].getBounds();\r
258 \r
259                     // If the bb height is not overridden with the font height\r
260                     // text lines will not be drawn in the correct Y location.\r
261                     rects[i].setRect(rects[i].getX(), rects[i].getY(), rects[i].getWidth(), fontHeight);\r
262 \r
263                     textHeight += rects[i].getHeight() * gScale;\r
264                     if (DEBUG)\r
265                         System.out.println("  bounding rectangle for line " + i + " '" + flagText[i] + "': " + rects[i]);\r
266                 }\r
267             }\r
268 \r
269             double leftoverHeight = textArea.getHeight() - textHeight;\r
270             if (leftoverHeight < 0)\r
271                 leftoverHeight = 0;\r
272 \r
273             if (DEBUG) {\r
274                 System.out.println("text area height: " + textArea.getHeight());\r
275                 System.out.println("total text height: " + textHeight);\r
276                 System.out.println("leftover height: " + leftoverHeight);\r
277             }\r
278 \r
279             double lineDist = 0;\r
280             double startY = 0;\r
281 \r
282             switch (vAlign) {\r
283                 case LEADING:\r
284                     if (DEBUG)\r
285                         System.out.println("VERTICAL LEADING");\r
286                     lineDist = leftoverHeight / flagText.length;\r
287                     startY = fm.getMaxAscent();\r
288                     break;\r
289                 case TRAILING:\r
290                     if (DEBUG)\r
291                         System.out.println("VERTICAL TRAILING");\r
292                     lineDist = leftoverHeight / flagText.length;\r
293                     startY = fm.getMaxAscent() + lineDist * gScaleRecip;\r
294                     break;\r
295                 case CENTER:\r
296                     if (DEBUG)\r
297                         System.out.println("VERTICAL CENTER");\r
298                     lineDist = leftoverHeight / (flagText.length + 1);\r
299                     startY = fm.getMaxAscent() + lineDist * gScaleRecip;\r
300                     break;\r
301             }\r
302 \r
303             if (DEBUG) {\r
304                 System.out.println("lineDist: " + lineDist);\r
305                 System.out.println("startY: " + startY);\r
306             }\r
307 \r
308             lineDist *= gScaleRecip;\r
309             double y = startY;\r
310             double textAreaWidth = textArea.getWidth() * gScaleRecip;\r
311 \r
312             for (int i = 0; i < flagText.length; ++i) {\r
313                 //String line = flagText[i];\r
314                 Rectangle2D rect = rects[i];\r
315 \r
316                 double x = 0;\r
317 \r
318                 switch (horizAlign) {\r
319                     case LEADING:\r
320                         if (DEBUG)\r
321                             System.out.println("HORIZ LEADING: " + rect);\r
322                         x = TEXT_MARGIN;\r
323                         break;\r
324                     case TRAILING:\r
325                         if (DEBUG)\r
326                             System.out.println("HORIZ TRAILING: " + rect);\r
327                         x = textAreaWidth - rect.getWidth() - TEXT_MARGIN;;\r
328                         break;\r
329                     case CENTER:\r
330                         if (DEBUG)\r
331                             System.out.println("HORIZ CENTER: " + rect);\r
332                         x = textAreaWidth * 0.5 - rect.getWidth()*0.5;\r
333                         break;\r
334                 }\r
335 \r
336                 if (DEBUG)\r
337                     System.out.println("  X, Y: " + x + ", " + y);\r
338 \r
339                 if (DEBUG)\r
340                     System.out.println(" DRAW: '" + flagText[i] + "' with " + g.getTransform());\r
341 \r
342                 //textLayout[i].draw(g, (float) x, (float) y);\r
343                 g.drawString(flagText[i], (float) x, (float) y);\r
344 \r
345                 y += lineDist;\r
346                 y += rect.getHeight();\r
347             }\r
348 \r
349         } finally {\r
350             g.setTransform(ot);\r
351         }\r
352     }\r
353 \r
354     public static double getBeakLength(double height, double beakAngle) {\r
355         beakAngle = Math.min(180, Math.max(10, beakAngle));\r
356         return height / (2*Math.tan(Math.toRadians(beakAngle) / 2));\r
357     }\r
358 \r
359     @Override\r
360     public Rectangle2D getBoundsInLocal() {\r
361         if (flagShape == null)\r
362             return null;\r
363         return flagShape.getBounds2D();\r
364     }\r
365 \r
366 }\r