View Javadoc

1   /*
2    * Copyright (c) 2003-2008 by Cosylab d. d.
3    *
4    * This file is part of CosyBeans-Common.
5    *
6    * CosyBeans-Common is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * CosyBeans-Common is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with CosyBeans-Common.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  
20  package com.cosylab.gui.components.range2;
21  
22  
23  /**
24   * Uses Range to find optimal tick distribution for LinearRange.
25   *
26   * @author <a href="mailto:nejc.kosnik@kgb.ijs.si">Nejc Kosnik</a>
27   * @version $id$
28   */
29  public class LinearTickCalculator implements TickCalculator
30  {
31  
32  	private static class TickStep
33  	{
34  		/** Factors for stretching and shrinking tickstep */
35  		public static final double[] FACTORS = { 2, 2.5, 2 };
36  		/** Current tickstep */
37  		private double tep;
38  		/** If the last performed stepchange has been stretching. */
39  		private boolean multipliedBefore;
40  		private int i;
41  		
42  		/**
43  		 * Shrinks tickstep.
44  		 */
45  		public void divStep()
46  		{
47  			if (multipliedBefore) {
48  				i = change(-1);
49  				multipliedBefore = false;
50  			}
51  
52  			tep /= FACTORS[i];
53  			i = change(-1);
54  		}
55  
56  		/**
57  		 * Stretches tickstep.
58  		 */
59  		public void mulStep()
60  		{
61  			if (!multipliedBefore) {
62  				i = change(1);
63  				multipliedBefore = true;
64  			}
65  
66  			tep *= FACTORS[i];
67  			i = change(+1);
68  		}
69  
70  		/**
71  		 * Creates a new TickStep object.
72  		 *
73  		 * @param step Initial step
74  		 * @param start Which index in <code>FACTORS</code> should be used for
75  		 *        stretching first.
76  		 * @param multipliedBefore Should be current
77  		 *        <code>FACTORS[start]</code> used for first stretching.
78  		 */
79  		public TickStep(double step, int start, boolean multipliedBefore)
80  		{
81  			i = start;
82  			this.tep = step;
83  			this.multipliedBefore = multipliedBefore;
84  		}
85  
86  		private int change(int di)
87  		{
88  			if (i == FACTORS.length - 1 && di == 1) {
89  				return 0;
90  			}
91  
92  			if (i == 0 && di == -1) {
93  				return 2;
94  			}
95  
96  			return i + di;
97  		}
98  	}
99  
100 	private int tickSpacing;
101 	private boolean boundTicksVisible;
102 
103 	/**
104 	 * Creates a new LinearTickCollector object.
105 	 *
106 	 * @param tickSpacing Minimum space between minor ticks.
107 	 * @param boundTicksVisible a flag indicating whether the range's bounds should be visible
108 	 */
109 	public LinearTickCalculator(int tickSpacing, boolean boundTicksVisible)
110 	{
111 		this.tickSpacing = tickSpacing;
112 		this.boundTicksVisible = boundTicksVisible;
113 	}
114 	
115 	/**
116 	 * @param length Available space in pixels.
117 	 * @param measurer
118 	 * @param range
119 	 * @param rangedVal
120 	 * @return
121 	 */
122 	private Tick[] internalCalculateTicks(int length, TickParameters measurer, Range range, RangedValue rangedVal)
123 	{	
124 		if (measurer == null) measurer = new DefaultTickParameters(50, "%0.6g");
125 		double min = rangedVal.getMinimum();
126 		double max = rangedVal.getMaximum();
127 		TickStep s = new TickStep(getRStep(min, max), 0, true);
128 		boolean zeroOnScale = isInRange(0, min, max);
129 		
130 		double rmin = getRmin(min, s, zeroOnScale);
131 		double rmax = getRmax(rmin, max, s);
132 
133 		int nticks = (int)((rmax - rmin) / s.tep) + 1;
134 
135 		double tickSpace = (range.toRelative(rmax,rangedVal)
136 			- range.toRelative(rmax - s.tep,rangedVal)) * length;
137 
138 		// try to increase major tick density
139 		while (tickSpace != 0 && nticks < 6 /* || tickSpace > 20*tickSpacing*/) {
140 			s.divStep();
141 			rmin = getRmin(min, s, zeroOnScale);
142 			rmax = getRmax(rmin,max, s);
143 			nticks = (int)((rmax - rmin) / s.tep) + 1;
144 			tickSpace = (range.toRelative(rmax,rangedVal)
145 				- range.toRelative(rmax - s.tep,rangedVal)) * length;
146 
147 			//	tickSpace = ((rmax - rmin) / (max - min) * length - nticks) / (nticks
148 			//		- 1);
149 		}
150 		
151 		//we might have to decrease major ticks count
152 		while (tickSpace < tickSpacing || nticks > 25) {
153 			s.mulStep();
154 			rmin = getRmin(rmin, s, zeroOnScale);
155 			rmax = getRmax(rmin, max, s);
156 
157 			if (rmin == rmax || Double.isNaN(rmin) || Double.isNaN(rmax)
158 			    || Double.isInfinite(rmin) || Double.isInfinite(rmax)) {
159 				if (boundTicksVisible) {
160 					return new Tick[]{
161 						new Tick(min, 0, true, ""), new Tick(max, 1, true, "")
162 					};
163 				} else if (zeroOnScale) {
164 					return new Tick[]{
165 						new Tick(0,range.toRelative(0,rangedVal), true,
166 								measurer.formatNumber(0))
167 					};
168 				} else {
169 					return new Tick[]{
170 						new Tick(rmin,range.toRelative(rmin,rangedVal), true,
171 								measurer.formatNumber(rmin))
172 					};
173 				}
174 			}
175 
176 			nticks = (int)((rmax - rmin) / s.tep) + 1;
177 			tickSpace = (range.toRelative(rmax,rangedVal)
178 				- range.toRelative(rmax - s.tep,rangedVal)) * length;
179 		}
180 
181 		//from now on we have to deal with labels
182 		double rstep = s.tep;
183 		TickStep ls = new TickStep(s.tep, s.i, s.multipliedBefore);
184 		double rminL = rmin;
185 		double rmaxL = rmax;
186 
187 		//measuring tick labels
188 		double x;
189 
190 		while (true) {
191 			// rstepL/10 is instead of zero to prevent round-off errors 
192 			for (x = rminL; x < rmaxL - ls.tep / 10; x += ls.tep) {
193 				if (measurer.measureTick(range.toRelative(x,rangedVal),
194 						measurer.formatNumber(x))
195 				    + measurer.measureTick(range.toRelative(x + ls.tep,rangedVal),
196 				    		measurer.formatNumber(x + ls.tep)) > 1.3 * (range
197 				    .toRelative(x + ls.tep,rangedVal) - range.toRelative(x,rangedVal)) * length) {
198 					break;
199 				}
200 			}
201 
202 			if (x >= rmaxL - ls.tep / 10) {
203 				break;
204 			} else {
205 				ls.mulStep();
206 				rminL = getRmin(min, ls, zeroOnScale);
207 				rmaxL = getRmax(rminL, max, ls);
208 			}
209 		}
210 
211 		if ((int)((rmaxL - rminL) / ls.tep) + 1 > 12) { //to many labeled ticks
212 			ls.mulStep();
213 			rminL = getRmin(min, ls, zeroOnScale);
214 			rmaxL = getRmax(rminL, max, ls);
215 		}
216 
217 		rstep = ls.tep / 2;
218 		rmin = getRmin(min, new TickStep(rstep, 0, true), zeroOnScale);
219 		rmax = getRmax(rmin, max, new TickStep(rstep, 0, true));
220 
221 		//minor ticks
222 		double minorStep = rstep / 5;
223 		double minorMin = getRmin(min, new TickStep(minorStep, 0, true), zeroOnScale);
224 		double minorMax = getRmax(minorMin, max, new TickStep(minorStep, 0, true));
225 
226 		if ((range.toRelative(minorMax,rangedVal)
227 		    - range.toRelative(minorMax - minorStep,rangedVal)) * length > tickSpacing) {
228 			//everything done already
229 		} else {
230 			minorStep = rstep / 2;
231 			minorMin = getRmin(min, new TickStep(minorStep, 0, true), zeroOnScale);
232 			minorMax = getRmax(minorMin, max, new TickStep(minorStep, 0, true));
233 
234 			if ((range.toRelative(minorMax,rangedVal)
235 			    - range.toRelative(minorMax - minorStep,rangedVal)) * length > tickSpacing) {
236 				//everything done already
237 			} else {
238 				minorStep = rstep;
239 			}
240 		}
241 
242 		minorMin = getRmin(min, new TickStep(minorStep, 0, true), zeroOnScale);
243 		minorMax = getRmax(minorMin, max, new TickStep(minorStep, 0, true));
244 
245 		nticks = Math.round(Math.round((rmax - rmin) / rstep) *	Math.round(rstep / minorStep)
246 			    + Math.round((minorMax - rmax + rmin - minorMin) / minorStep)) + 1;
247 
248 		if (nticks == 0) {
249 			Tick[] ticks = new Tick[1];
250 			ticks[0] = new Tick(0,range.toRelative(0,rangedVal), true, measurer.formatNumber(0));
251 
252 			return ticks;
253 		}
254 
255 		int i = 0;
256 		int di = 0;
257 
258 		if (boundTicksVisible) {
259 			if ((rmin - min) / (max - min) > 0.01
260 			    || (max - rmin) / (max - min) > 0.01) {
261 				nticks += 2;
262 				i = 1;
263 				di = 1;
264 			}
265 		}
266 
267 		Tick[] ticks = new Tick[nticks];
268 
269 		if (i == 1) {
270 			//			this tick was unlabelled originally
271 			ticks[0] = new Tick(rmin, 0.0, true, "");
272 		}
273 
274 		int ratio2 = (int)Math.round(ls.tep / minorStep);
275 		int ratio1 = (int)Math.round(rstep / minorStep);
276 		x = minorMin;
277 
278 		for (; x < rmin - minorStep / 10; i++, x += minorStep) {
279 			ticks[i] = new Tick(x, range.toRelative(x,rangedVal), false);
280 		}
281 
282 		int cont = 0;
283 		
284 		for (int k = 0; x < rminL - minorStep / 10; i++, k++, x += minorStep) {
285 			if (k % ratio1 == 0) {
286 				x = rmin + (cont++) * rstep; //to prevent round off errors
287 				ticks[i] = new Tick(x, range.toRelative(x,rangedVal), true, "");
288 			} else {
289 				ticks[i] = new Tick(x, range.toRelative(x,rangedVal), false);
290 			}
291 		}
292 
293 		for (int k = 0; i < nticks - di; i++, k++, x += minorStep) {
294 			if (k % ratio2 == 0) {
295 				x = rmin + (cont++) * rstep; //to prevent round off errors
296 				x = (Math.abs(x) != 0.0 ? x : Math.abs(x)); //to prevent ugly negative zero representation -0.0 
297 				ticks[i] = new Tick(x, range.toRelative(x,rangedVal), true,
298 					    /*formatter.sprintf(x)*/ measurer.formatNumber(x));
299 			} else if (k % ratio1 == 0) {
300 				x = rmin + (cont++) * rstep; //to prevent round off errors
301 				ticks[i] = new Tick(x, range.toRelative(x,rangedVal), true, "");
302 			} else {
303 				ticks[i] = new Tick(x, range.toRelative(x,rangedVal), false);
304 			}
305 		}
306 
307 		if (di == 1) {
308 			//this tick was unlabelled originally
309 			ticks[i] = new Tick(rmax, 1.0, true, "");
310 		}
311 		
312 		return ticks;
313 	}
314 	
315 	private double getRmin(double min, TickStep s, boolean zeroOnScale)
316 	{
317 		double rmin;
318 
319 		if (zeroOnScale) {
320 			rmin = -Math.floor(-min / s.tep) * s.tep;
321 
322 			if (rmin == -0.0) {
323 				rmin = 0.0;
324 			}
325 		} else {
326 			rmin = (min < 0 ? -Math.floor(-min / s.tep) * s.tep
327 				: Math.ceil(min / s.tep) * s.tep);
328 		}
329 
330 		return rmin;
331 	}
332 
333 	private double getRmax(double rmin, double max, TickStep s)
334 	{
335 		// conversion to float makes rounder values
336 		return rmin + Math.floor((float)((max - rmin) / s.tep)) * s.tep;
337 	}
338 
339 	private double getRStep(double min, double max)
340 	{
341 		double rstep;
342 
343 		if (Math.abs(max) > Math.abs(min)) {
344 			rstep = Math.pow(10,
345 				    Math.floor(Math.log(Math.abs(max)) / Math.log(10)));
346 		} else {
347 			rstep = Math.pow(10,
348 				    Math.floor(Math.log(Math.abs(min)) / Math.log(10)));
349 		}
350 
351 		return rstep;
352 	}
353 
354 	private boolean isInRange(double value, double min, double max)
355 	{
356 		return (value >= min && value <= max);
357 	}
358 
359 	/*
360 	 * (non-Javadoc)
361 	 * @see com.cosylab.gui.components.range2.TickCalculator#calculateTicks(com.cosylab.gui.components.range2.Range, com.cosylab.gui.components.range2.RangedValue)
362 	 */
363 	public Tick[] calculateTicks(int length, Range range, RangedValue rangedValue) {
364 		return calculateTicks(length, new DefaultTickParameters(50, "%0.6g"), range, rangedValue);
365 	}
366 
367 	/*
368 	 * (non-Javadoc)
369 	 * @see com.cosylab.gui.components.range2.TickCalculator#calculateTicks(com.cosylab.gui.components.range2.TickParameters, com.cosylab.gui.components.range2.Range, com.cosylab.gui.components.range2.RangedValue)
370 	 */
371 	public Tick[] calculateTicks(int length, TickParameters measurer, Range range, RangedValue rangedValue) {
372 		return internalCalculateTicks(length, measurer, range, rangedValue);
373 	}
374 }
375 
376 /* __oOo__ */