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