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