/*
* Copyright (c) 2008 Stiftung Deutsches Elektronen-Synchrotron,
* Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY.
*
* THIS SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "../AS IS" BASIS.
* WITHOUT WARRANTY OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR PARTICULAR PURPOSE AND
* NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE. SHOULD THE SOFTWARE PROVE DEFECTIVE
* IN ANY RESPECT, THE USER ASSUMES THE COST OF ANY NECESSARY SERVICING, REPAIR OR
* CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE.
* NO USE OF ANY SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
* DESY HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS,
* OR MODIFICATIONS.
* THE FULL LICENSE SPECIFYING FOR THE SOFTWARE THE REDISTRIBUTION, MODIFICATION,
* USAGE AND OTHER RIGHTS AND OBLIGATIONS IS INCLUDED WITH THE DISTRIBUTION OF THIS
* PROJECT IN THE FILE LICENSE.HTML. IF THE LICENSE IS NOT INCLUDED YOU MAY FIND A COPY
* AT HTTP://WWW.DESY.DE/LEGAL/LICENSE.HTM
*/
package org.csstudio.sds.components.ui.internal.figures;
import java.util.ArrayList;
import java.util.List;
/**
* <p>Calculates where the tickmarks on an axis should be placed. Ticks are
* placed at "nice numbers", i.e. their distance is one of the
* following:</p>
*
* <ul>
* <li>1.0 * 10<sup>n</sup></li>
* <li>2.0 * 10<sup>n</sup></li>
* <li>2.5 * 10<sup>n</sup></li>
* <li>5.0 * 10<sup>n</sup></li>
* </ul>
*
* <p><strong>Warning:</strong> Objects of this class currently do not actually
* place tickmarks at nice numbers because the algorithm to do that turned out
* to not work very well. The current algorithm simply tries to reduce the
* number of significant decimal places of the tickmark distance.</p>
*
* <p>Usage: set the minimum and maximum data value and the maximum number of
* tickmarks. Then, get the smallest value at which to place a tickmark and the
* distance of tickmarks.</p>
*
* @author Joerg Rathlev
*/
class TickCalculator {
/**
* The smallest data value.
*/
private double _min = 0.0;
/**
* The largest data value.
*/
private double _max = 0.0;
/**
* The maximum number of tickmarks, as set by the client.
*/
private int _maxTickCount;
/**
* The smallest data value at which a tickmark should be placed.
*/
private double _smallestTick;
/**
* The distance at which the tickmarks should be placed.
*/
private double _tickDistance;
/**
* Whether this calculator should create ticks only at integral values.
*/
private boolean _integerOnly;
/**
* Sets the minimum value in the dataset.
* @param d the minimum value in the dataset.
*/
public void setMinimumValue(final double d) {
this._min = d;
recalculate();
}
/**
* Sets the maximum value in the dataset.
* @param d the maximum value in the dataset.
*/
public void setMaximumValue(final double d) {
this._max = d;
recalculate();
}
/**
* Sets the maximum number of tickmarks for which this calculator will
* calculate positions.
* @param i the maximum number of tickmarks.
*/
public void setMaximumTickCount(final int i) {
this._maxTickCount = i;
recalculate();
}
/**
* Sets whether this calculator should create ticks only at integral values.
* @param integerOnly the setting.
*/
public void setIntegerOnly(final boolean integerOnly) {
_integerOnly = integerOnly;
recalculate();
}
/**
* Calculates and returns the major ticks.
*
* @return the major ticks.
*/
public List<Tick> calculateTicks() {
List<Tick> result = new ArrayList<Tick>();
recalculate();
if (_tickDistance == 0.0 || _min > _max || _maxTickCount < 2) {
return result;
}
double value = _smallestTick;
while (value <= _max) {
Tick tick = new Tick(TickType.MAJOR, value);
result.add(tick);
value += _tickDistance;
}
return result;
}
/**
* Recalculates the positions of the tickmarks based on the provided
* minimum and maximum data values and maximum number of tickmarks.
*/
private void recalculate() {
if (_min > _max || _maxTickCount < 2) {
return;
}
double dataRange = _max - _min;
// if we were to create exactly maxTickCount ticks, without any
// snapping to nice numbers, this would be their distance:
double exactDist = dataRange / (_maxTickCount - 1);
// Now, the goal is to find the smallest distance that is larger than
// or equal to the exact distance, and that is a "nice number".
// the order of magnitude of the exact distance:
double o = orderOfMagnitude(exactDist);
// Ok, this is basically the algorithm by Kay Kasemir from his
// org.csstudio.swt.charts component. We're looking at the order
// of magnitude of the exact distance, and then rounding so that
// the actual distance of the tickmarks will have one additional
// significant decimal place. For example, if the order of magnitude
// of the exact distance is -1, the distance of the tickmarks will
// have two decimal places. Note that this works better the more
// tickmarks you have. With a small number of tickmarks, this will
// almost certainly NOT place them at nice numbers.
_tickDistance = Math.ceil(exactDist / Math.pow(10, o - 1)) * Math.pow(10, o - 1);
if (_integerOnly) {
_tickDistance = Math.ceil(_tickDistance);
}
// The algorithm below rounds to nice numbers, but is overly
// aggressive, so it is disabled for now.
// TODO: find a better algorithm.
// Here is an example for the problem with the algorithm below:
// With a minimum of -130 and a maximum of 170 and a maximum of
// four tickmarks, the exact distance would be 100 and that is what
// is actually used, so tickmarks will be placed at -100, 0 and 100.
// But if the maximum is increased to 171, the exact distance will be
// slightly larger than 100, and will be rounded up to 200, so the
// tickmarks would be placed at -200, 0 and 200, but both -200 and
// 200 are outside the bounds of the graph! So there will be only one
// tickmark at 0. The best solution would be to still place the
// tickmarks at -100, 0 and 100.
// // the (decimal) mantissa of the exact distance:
// double m = exactDist / Math.pow(10, o);
//
// // the exact distance is now
// // m * 10^o
// // Increase m to the nearest "nice number" mantissa, or if m is already
// // larger than 5.0, set m to 1.0 and increase the order of magnitude.
// if (m <= 1.0) {
// m = 1.0;
// } else if (m <= 2.0) {
// m = 2.0;
// } else if (m <= 2.5) {
// m = 2.5;
// } else if (m <= 5.0) {
// m = 5.0;
// } else {
// m = 1.0;
// o += 1;
// }
// tickDistance = m * Math.pow(10, o);
//
// The first tick must be shown at the smallest value that is larger
// than or equal to the minimum value and a multiple of the interval.
// This ensures that the ticks really are shown at "nice" numbers, even
// if the smallest data value is not a nice number, and it ensures that
// a tick is always shown at 0.0.
_smallestTick = Math.ceil(_min / _tickDistance) * _tickDistance;
}
/**
* Returns the order of magnitude of the given value.
*
* @param d a value.
* @return the order of magnitude of d.
*/
private static double orderOfMagnitude(final double d) {
if (d == 0) {
return 0.0;
} else {
return Math.floor(Math.log10(Math.abs(d)));
}
}
}