f7e0d1e5c6b5cb05d280b9a5ae514758723b2370
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / RulerNode.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.AlphaComposite;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Font;
18 import java.awt.FontMetrics;
19 import java.awt.Graphics2D;
20 import java.awt.geom.AffineTransform;
21 import java.awt.geom.Rectangle2D;
22 import java.util.Locale;
23
24 import org.simantics.scenegraph.g2d.G2DNode;
25 import org.simantics.scenegraph.utils.GridUtils;
26
27 public class RulerNode extends G2DNode {
28     /**
29      * 
30      */
31     private static final long  serialVersionUID  = 2490944880914577411L;
32
33     /**
34      * FIXME: this is a hack for the map UI that has to be solved some other way.
35      */
36     private static final boolean MAP_Y_SCALING = false;
37
38     private static final Color GRAY              = new Color(100, 100, 100);
39
40     protected Boolean          enabled           = true;
41
42     protected double           gridSize          = 1.0;
43
44     @SyncField("enabled")
45     public void setEnabled(Boolean enabled) {
46         this.enabled = enabled;
47     }
48
49     @SyncField("gridSize")
50     public void setGridSize(double gridSize) {
51         if (gridSize < 1e-6)
52             gridSize = 1e-6;
53         this.gridSize = gridSize;
54     }
55
56     @Override
57     public void render(Graphics2D g) {
58         if (!enabled)
59             return;
60
61         AffineTransform tr = g.getTransform();
62         double scaleX = Math.abs(tr.getScaleX());
63         double scaleY = Math.abs(tr.getScaleY());
64         if (scaleX <= 0 || scaleY <= 0) {
65             // Make sure that we don't end up in an eternal loop below.
66             return;
67         }
68         double offsetX = tr.getTranslateX();
69         double offsetY = tr.getTranslateY();
70         g.setTransform(new AffineTransform());
71
72         Font rulerFont = new Font("Tahoma", Font.PLAIN, 9);
73
74         //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
75         g.setStroke(new BasicStroke(1));
76         g.setColor(new Color(0.9f, 0.9f, 0.9f, 0.75f));
77
78         Rectangle2D bounds = g.getClipBounds();
79         if(bounds == null) return; // FIXME
80
81         g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
82
83         Rectangle2D vertical = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMinY()+20);
84         g.fill(vertical);
85
86         Rectangle2D horizontal = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY()+20, bounds.getMinX()+20, bounds.getMaxY());
87         g.fill(horizontal);
88
89         // stepX and stepY should be something between 50 and 100
90         double stepX = 50;
91         double stepY = 50;
92
93         stepX = GridUtils.limitedEvenGridSpacing(stepX, scaleX, 100, gridSize, true);
94         stepY = GridUtils.limitedEvenGridSpacing(stepY, scaleY, 100, gridSize, true);
95         //while(stepX * scaleX > 100) stepX /= 2;
96         //while(stepY * scaleY > 100) stepY /= 2;
97
98         while(stepX * scaleX < 50) stepX *= 2;
99         while(stepY * scaleY < 50) stepY *= 2;
100
101         stepX *= scaleX;
102         stepY *= scaleY;
103
104         g.setColor(GRAY);
105         g.setFont(rulerFont);
106
107         double previousText = -100;
108
109         // Vertical ruler
110         for(double x = offsetX%stepX-stepX; x < bounds.getMaxX(); x+=stepX) {
111             if(x > 20) {
112                 String str = formatValue((x-offsetX)/scaleX);
113                 FontMetrics fm = g.getFontMetrics();
114                 Rectangle2D r = fm.getStringBounds(str, g);
115                 if((x-r.getWidth()/2) > previousText) {
116                     g.setColor(Color.BLACK);
117                     g.drawString(str, (int)(x-r.getWidth()/2), (int)(bounds.getMinY()+1+r.getHeight()));
118                     previousText = x+r.getWidth()/2+stepX/4;
119                 }
120
121                 g.setColor(GRAY);
122                 g.drawLine((int)x, (int)bounds.getMinY()+12, (int)x, (int)bounds.getMinY()+19);
123             }
124             if(stepX/5 > 2) {
125                 for(double x2 = x+stepX/5; x2 < x+stepX; x2+=stepX/5) {
126                     if(x2 > 20) {
127                         g.drawLine((int)x2, (int)bounds.getMinY()+15, (int)x2, (int)bounds.getMinY()+19);
128                     }
129                 }
130                 for(double x2 = x+stepX/10; x2 < x+stepX; x2+=stepX/5) {
131                     if(x2 > 20) {
132                         g.drawLine((int)x2, (int)bounds.getMinY()+17, (int)x2, (int)bounds.getMinY()+19);
133                     }
134                 }
135
136             }
137         }
138
139         // Horizontal ruler
140         previousText = -100;
141         for(double y = offsetY%stepY-stepY; y < bounds.getMaxY(); y+=stepY) {
142             if(y > 20) {
143                 double val = (y-offsetY)/scaleY;
144                 if (MAP_Y_SCALING)
145                     val = Math.toDegrees(Math.atan(Math.sinh(Math.toRadians(val))));
146                 String str = formatValue(val);
147                 FontMetrics fm = g.getFontMetrics();
148                 Rectangle2D r = fm.getStringBounds(str, g);
149                 if(y-1+r.getHeight()/2 > previousText) {
150                     g.setColor(Color.BLACK);
151                     AffineTransform origTr = g.getTransform();
152                     g.translate((int)(bounds.getMinX()), (int)(y+r.getWidth()/2));
153                     g.rotate(-Math.PI / 2.0);
154                     g.drawString(str, 0, (int)r.getHeight());
155                     g.setTransform(origTr);
156                     previousText = y-1+r.getHeight();
157                 }
158                 g.setColor(GRAY);
159                 g.drawLine((int)bounds.getMinX()+12, (int)y, (int)bounds.getMinX()+19, (int)y);
160             }
161             if(stepY/5 > 2) {
162                 for(double y2 = y+stepY/5; y2 < y+stepY; y2+=stepY/5) {
163                     if(y2 > 20) {
164                         g.drawLine((int)bounds.getMinX()+15, (int)y2, (int)bounds.getMinX()+19, (int)y2);
165                     }
166                 }
167                 for(double y2 = y+stepY/10; y2 < y+stepY; y2+=stepY/5) {
168                     if(y2 > 20) {
169                         g.drawLine((int)bounds.getMinX()+17, (int)y2, (int)bounds.getMinX()+19, (int)y2);
170                     }
171                 }
172             }
173         }
174
175         g.setTransform(tr);
176     }
177
178     private static final transient int    MAX_DIGITS = 5;
179     private static final transient double EPSILON    = 0.01;
180     private static final transient double TRIM_THRESHOLD_MAX_VALUE = Math.pow(10, 4);
181     private static final transient String[] SI_UNIT_LARGE_PREFIXES = {
182         "k", "M", "G", "T", "P", "E", "Z", "Y"
183     };
184
185     public static String formatValue(double value) {
186         int magnitude = (int) Math.round(Math.log10(value));
187         //System.out.println("magnitude: " + magnitude + ", " + value);
188         int allowedDecimals = MAX_DIGITS;
189         allowedDecimals -= Math.abs(magnitude);
190         if (allowedDecimals < 0)
191             allowedDecimals = 0;
192
193         String valueStr = String.format(Locale.US, "%." + allowedDecimals + "f", value);
194         if (allowedDecimals > 0) {
195             for (int trunc = valueStr.length() - 1; trunc > 0; --trunc) {
196                 char ch = valueStr.charAt(trunc);
197                 if (ch == '.') {
198                     valueStr = valueStr.substring(0, trunc);
199                     break;
200                 }
201                 if (valueStr.charAt(trunc) != '0') {
202                     valueStr = valueStr.substring(0, trunc + 1);
203                     break;
204                 }
205             }
206             if (Math.abs(value) + EPSILON > TRIM_THRESHOLD_MAX_VALUE) {
207                 // Cut anything beyond a possible decimal dot out since they
208                 // should not show anyway. This is a complete hack that tries to
209                 // circumvent floating-point inaccuracy problems.
210                 int dotIndex = valueStr.lastIndexOf('.');
211                 if (dotIndex > -1) {
212                     valueStr = valueStr.substring(0, dotIndex);
213                 }
214             }
215         }
216
217         double trimValue = value;
218         if (Math.abs(value)+EPSILON >= TRIM_THRESHOLD_MAX_VALUE) {
219             for (int i = 0; Math.abs(trimValue)+EPSILON >= TRIM_THRESHOLD_MAX_VALUE; ++i) {
220                 double trim = trimValue / 1000;
221                 if (Math.abs(trim)-EPSILON < TRIM_THRESHOLD_MAX_VALUE) {
222                     valueStr = valueStr.substring(0, valueStr.length() - (i + 1) * 3);
223                     valueStr += SI_UNIT_LARGE_PREFIXES[i];
224                     break;
225                 }
226                 trimValue = trim;
227             }
228         }
229
230         if (valueStr.equals("-0"))
231             valueStr = "0";
232
233         return valueStr;
234     }
235
236     @Override
237     public Rectangle2D getBoundsInLocal() {
238         return null;
239     }
240
241 }