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