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
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.scenegraph.g2d.nodes;
\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
29 import org.simantics.scenegraph.g2d.G2DNode;
\r
30 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
\r
31 import org.simantics.scenegraph.utils.GeometryUtils;
\r
33 public class FlagNode extends G2DNode {
\r
35 private static final long serialVersionUID = -1716729504104107151L;
\r
37 private static final AffineTransform IDENTITY = new AffineTransform();
\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
43 private static final boolean DEBUG = false;
\r
45 private static final double GLOBAL_SCALE = 0.1;
\r
47 private static final double TEXT_MARGIN = 5;
\r
49 static transient final BasicStroke STROKE = new BasicStroke(0.25f, BasicStroke.CAP_BUTT,
\r
50 BasicStroke.JOIN_MITER);
\r
52 final transient Font FONT = Font.decode("Arial 12");
\r
54 protected boolean visible;
\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
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
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
79 @SyncField("visible")
\r
80 public void setVisible(boolean visible) {
\r
81 this.visible = visible;
\r
84 public boolean isVisible() {
\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
96 this.textColor = textColor;
\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
108 private void resetCaches() {
\r
114 public void render(Graphics2D g) {
\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
136 AffineTransform ot = g.getTransform();
\r
137 g.transform(transform);
\r
140 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
\r
142 //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
\r
144 // Paint flag shape
\r
147 g.setStroke(stroke);
\r
148 g.setColor(border);
\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
159 if (flagText == null || flagText.length == 0)
\r
163 g.setColor(Color.RED);
\r
170 g.setColor(textColor);
\r
172 AffineTransform orig = g.getTransform();
\r
174 double det = orig.getDeterminant();
\r
176 System.out.println("DETERMINANT: " + det);
\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
184 origin.setLocation(textArea.getMinX(), textArea.getMinY());
\r
185 xa.setLocation(textArea.getMaxX(), textArea.getMinY());
\r
186 ya.setLocation(textArea.getMinX(), textArea.getMaxY());
\r
189 orig.transform(origin, origin);
\r
190 orig.transform(xa, xa);
\r
191 orig.transform(ya, ya);
\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
198 boolean needToFlip = xAxisX < 0 || yAxisY < 0;
\r
200 System.out.println("TEXT NEEDS FLIPPING: " + needToFlip);
\r
202 byte horizAlign = hAlign;
\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
216 // Must flip horizontal alignment to keep text visually at the same
\r
218 if (horizAlign == LEADING)
\r
219 horizAlign = TRAILING;
\r
220 else if (horizAlign == TRAILING)
\r
221 horizAlign = LEADING;
\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
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
243 FontMetrics fm = g.getFontMetrics(f);
\r
244 double fontHeight = fm.getHeight();
\r
246 if (textLayout == null || (float) scale != lastViewScale)
\r
248 lastViewScale = (float) scale;
\r
249 FontRenderContext frc = g.getFontRenderContext();
\r
250 if (textLayout == null)
\r
251 textLayout = new TextLayout[flagText.length];
\r
253 rects = new Rectangle2D[flagText.length];
\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
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
264 textHeight += rects[i].getHeight() * gScale;
\r
266 System.out.println(" bounding rectangle for line " + i + " '" + flagText[i] + "': " + rects[i]);
\r
270 double leftoverHeight = textArea.getHeight() - textHeight;
\r
271 if (leftoverHeight < 0)
\r
272 leftoverHeight = 0;
\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
280 double lineDist = 0;
\r
286 System.out.println("VERTICAL LEADING");
\r
287 lineDist = leftoverHeight / flagText.length;
\r
288 startY = fm.getMaxAscent();
\r
292 System.out.println("VERTICAL TRAILING");
\r
293 lineDist = leftoverHeight / flagText.length;
\r
294 startY = fm.getMaxAscent() + lineDist * gScaleRecip;
\r
298 System.out.println("VERTICAL CENTER");
\r
299 lineDist = leftoverHeight / (flagText.length + 1);
\r
300 startY = fm.getMaxAscent() + lineDist * gScaleRecip;
\r
305 System.out.println("lineDist: " + lineDist);
\r
306 System.out.println("startY: " + startY);
\r
309 lineDist *= gScaleRecip;
\r
311 double textAreaWidth = textArea.getWidth() * gScaleRecip;
\r
312 boolean isRenderingPdf = g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER) != null;
\r
314 for (int i = 0; i < flagText.length; ++i) {
\r
315 //String line = flagText[i];
\r
316 Rectangle2D rect = rects[i];
\r
320 switch (horizAlign) {
\r
323 System.out.println("HORIZ LEADING: " + rect);
\r
328 System.out.println("HORIZ TRAILING: " + rect);
\r
329 x = textAreaWidth - rect.getWidth() - TEXT_MARGIN;;
\r
333 System.out.println("HORIZ CENTER: " + rect);
\r
334 x = textAreaWidth * 0.5 - rect.getWidth()*0.5;
\r
339 System.out.println(" X, Y: " + x + ", " + y);
\r
342 System.out.println(" DRAW: '" + flagText[i] + "' with " + g.getTransform());
\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
348 textLayout[i].draw(g, (float) x, (float) y);
\r
351 y += rect.getHeight();
\r
355 g.setTransform(ot);
\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
365 public Rectangle2D getBoundsInLocal() {
\r
366 if (flagShape == null)
\r
368 return flagShape.getBounds2D();
\r