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