/*
* Copyright 2012 Diamond Light Source Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.csstudio.swt.xygraph.linearscale;
import java.util.ArrayList;
import java.util.List;
import org.csstudio.swt.xygraph.linearscale.TickFactory.TickFormatting;
import org.eclipse.draw2d.geometry.Dimension;
/**
* Class to represent a major tick
*/
class Tick {
private String text;
private double value;
private double position;
private int tPosition;
/**
* @param tickText
*/
public void setText(String tickText) {
text = tickText;
}
/**
* @return the tick text
*/
public String getText() {
return text;
}
/**
* @param tickValue
*/
public void setValue(double tickValue) {
value = tickValue;
}
/**
* @return the tick value
*/
public double getValue() {
return value;
}
/**
* @param tickPosition in pixels
*/
public void setPosition(double tickPosition) {
position = tickPosition;
}
/**
* @return the tick position in pixels
*/
public double getPosition() {
return position;
}
/**
* @param textPosition in pixels
*/
public void setTextPosition(int textPosition) {
tPosition = textPosition;
}
/**
* @return the text position in pixels
*/
public int getTextPosition() {
return tPosition;
}
@Override
public String toString() {
return text + " (" + value + ", " + position + ", " + tPosition + ")";
}
}
public class LinearScaleTicks2 implements ITicksProvider {
protected List<Tick> ticks;
/** the maximum width of tick labels */
private int maxWidth;
/** the maximum height of tick labels */
private int maxHeight;
/** the array of minor tick positions in pixels */
protected ArrayList<Integer> minorPositions;
protected IScaleProvider scale;
private boolean ticksIndexBased;
public LinearScaleTicks2(IScaleProvider scale) {
this.scale = scale;
minorPositions = new ArrayList<Integer>();
}
@Override
public List<Integer> getPositions() {
List<Integer> positions = new ArrayList<Integer>();
for (Tick t : ticks)
positions.add((int) t.getPosition());
return positions;
}
@Override
public int getPosition(int index) {
return (int) ticks.get(index).getPosition();
}
@Override
public int getLabelPosition(int index) {
return ticks.get(index).getTextPosition();
}
@Override
public double getValue(int index) {
return ticks.get(index).getValue();
}
@Override
public String getLabel(int index) {
return ticks.get(index).getText();
}
@Override
public boolean isVisible(int index) {
return true;
}
@Override
public int getMajorCount() {
return ticks == null ? 0 : ticks.size();
}
@Override
public int getMinorCount() {
return minorPositions.size();
}
@Override
public int getMinorPosition(int index) {
return minorPositions.get(index);
}
@Override
public int getMaxWidth() {
return maxWidth;
}
@Override
public int getMaxHeight() {
return maxHeight;
}
private final static int TICKMINDIST_IN_PIXELS_X = 40;
private final static int TICKMINDIST_IN_PIXELS_Y = 30;
private final static int MIN_TICKS = 3;
@Override
public Range update(final double min, final double max, final int length) {
if (scale.isLogScaleEnabled() && (min <= 0 || max <= 0))
throw new IllegalArgumentException("Range for log scale must be in positive range");
final int maximumNumTicks = scale.isHorizontal() ? Math.min(8, length / TICKMINDIST_IN_PIXELS_X)
: Math.min(8, length / TICKMINDIST_IN_PIXELS_Y);
int numTicks = Math.max(3, maximumNumTicks);
final TickFactory tf;
if (scale instanceof AbstractScale) {
AbstractScale aScale = (AbstractScale) scale;
if (aScale.hasUserDefinedFormat()) {
tf = new TickFactory(scale);
} else if (aScale.isAutoFormat()) {
tf = new TickFactory(TickFormatting.autoMode, scale);
} else {
String format = aScale.getFormatPattern();
if (format.contains("E")) {
tf = new TickFactory(TickFormatting.useExponent, scale);
} else {
tf = new TickFactory(TickFormatting.autoMode, scale);
}
}
} else {
tf = new TickFactory(TickFormatting.autoMode, scale);
}
final int hMargin = getHeadMargin();
final int tMargin = getTailMargin();
// loop until labels fit
do {
if (ticksIndexBased) {
ticks = tf.generateIndexBasedTicks(min, max, numTicks, !scale.hasTicksAtEnds());
} else if (scale.isLogScaleEnabled()) {
ticks = tf.generateLogTicks(min, max, numTicks, true, !scale.hasTicksAtEnds());
} else {
ticks = tf.generateTicks(min, max, numTicks, true, !scale.hasTicksAtEnds());
}
} while (!updateLabelPositionsAndCheckGaps(length, hMargin, tMargin, min > max) && numTicks-- > MIN_TICKS);
updateMinorTicks(hMargin+length);
if (scale.hasTicksAtEnds() && ticks.size() > 1)
return new Range(ticks.get(0).getValue(), ticks.get(ticks.size() - 1).getValue());
return null;
}
@Override
public String getDefaultFormatPattern(double min, double max) {
String format = null;
// calculate the default decimal format
double mantissa = Math.abs(max - min);
if (mantissa > 0.1)
format = "############.##";
else {
format = "##.##";
while (mantissa < 1 && mantissa > 0) {
mantissa *= 10.0;
format += "#";
}
}
return format;
}
@Override
public int getHeadMargin() {
if (ticks == null || ticks.size() == 0 || maxWidth == 0 || maxHeight == 0) {
// System.err.println("No ticks yet!");
final Dimension l = scale.calculateDimension(scale.getScaleRange().getLower());
if (scale.isHorizontal()) {
// System.err.println("calculate X margin with " + r);
return l.width;
}
// System.err.println("calculate Y margin with " + r);
return l.height;
}
return scale.isHorizontal() ? (maxWidth + 1) / 2 : (maxHeight + 1) / 2;
}
@Override
public int getTailMargin() {
if (ticks == null || ticks.size() == 0 || maxWidth == 0 || maxHeight == 0) {
// System.err.println("No ticks yet!");
final Dimension h = scale.calculateDimension(scale.getScaleRange().getUpper());
if (scale.isHorizontal()) {
// System.err.println("calculate X margin with " + r);
return h.width;
}
// System.err.println("calculate Y margin with " + r);
return h.height;
}
return scale.isHorizontal() ? (maxWidth + 1) / 2 : (maxHeight + 1) / 2;
}
private static final String MINUS = "-";
/**
* Update positions and max dimensions of tick labels
*
* @return true if there is no overlaps
*/
private boolean updateLabelPositionsAndCheckGaps(int length, final int hMargin, final int tMargin, final boolean isReversed) {
final int imax = ticks.size();
if (imax == 0) {
return true;
}
maxWidth = 0;
maxHeight = 0;
final boolean hasNegative = ticks.get(0).getText().startsWith(MINUS);
final int minus = scale.calculateDimension(MINUS).width;
for (Tick t : ticks) {
final String l = t.getText();
final Dimension d = scale.calculateDimension(l);
if (hasNegative && !l.startsWith(MINUS)) {
d.width += minus;
}
if (d.width > maxWidth) {
maxWidth = d.width;
}
if (d.height > maxHeight) {
maxHeight = d.height;
}
}
if (length <= 0)
return true; // sanity check
// System.err.println("Max labels have w:" + maxWidth + ", h:" + maxHeight);
if (isReversed) {
for (Tick t : ticks) {
t.setPosition(length - length * t.getPosition() + hMargin);
}
} else {
for (Tick t : ticks) {
t.setPosition(length * t.getPosition() + hMargin);
}
}
length += hMargin + tMargin; // re-expand length (so labels can flow into margins)
if (scale.isHorizontal()) {
final int space = (int) (0.67 * scale.calculateDimension(" ").width);
int last = 0;
for (Tick t : ticks) {
final Dimension d = scale.calculateDimension(t.getText());
int w = d.width;
int p = (int) Math.ceil(t.getPosition() - w * 0.5);
if (p < 0) {
p = 0;
} else if (p + w >= length) {
p = length - 1 - w;
}
t.setTextPosition(p);
if (last > p) {
if (ticks.indexOf(t) == (imax - 1) || imax > MIN_TICKS) {
return false;
} else {
t.setText("");
}
} else {
last = p + w + space;
}
}
} else {
for (Tick t : ticks) {
final Dimension d = scale.calculateDimension(t.getText());
int h = d.height;
int p = (int) Math.ceil(length - 1 - t.getPosition() - h * 0.5);
if (p < 0) {
p = 0;
} else if (p + h >= length) {
p = length - 1 - h;
}
t.setTextPosition(p);
}
}
return true;
}
private static final double LAST_STEP_FRAC = 1 - Math.log10(9); // fraction of major tick step between 9 and 10
private void updateMinorTicks(final int end) {
minorPositions.clear();
final int jmax = ticks.size();
if (jmax <= 1)
return;
double majorStepInPixel = (ticks.get(jmax-1).getPosition() - ticks.get(0).getPosition()) / (jmax - 1);
if (majorStepInPixel == 0)
return;
int minorTicks;
if (scale.isLogScaleEnabled()) {
if (majorStepInPixel * LAST_STEP_FRAC >= scale.getMinorTickMarkStepHint()) {
minorTicks = 10 * (int) Math.abs(Math.log10(ticks.get(1).getValue() / ticks.get(0).getValue()));
double p = ticks.get(0).getPosition();
if (p > 0) {
p -= majorStepInPixel;
for (int i = 1; i < minorTicks; i++) {
int q = (int) (p + majorStepInPixel * Math.log10((10. * i) / minorTicks));
if (q >= 0 && q < end)
minorPositions.add(q);
}
}
for (int j = 0; j < jmax; j++) {
p = ticks.get(j).getPosition();
for (int i = 1; i < minorTicks; i++) {
int q = (int) (p + majorStepInPixel * Math.log10((10. * i) / minorTicks));
if (q >= 0 && q < end)
minorPositions.add(q);
}
}
}
} else {
double step = Math.abs(majorStepInPixel);
if (ticksIndexBased) {
minorTicks = (int) Math.abs(ticks.get(1).getValue() - ticks.get(0).getValue());
if (minorTicks == 1)
return;
if (minorTicks > step/5) {
if (step / 5 >= scale.getMinorTickMarkStepHint()) {
minorTicks = 5;
} else if (step / 4 >= scale.getMinorTickMarkStepHint()) {
minorTicks = 4;
} else {
minorTicks = 2;
}
} else if (minorTicks > 5)
minorTicks = 5;
} else {
if (scale.isDateEnabled()) {
minorTicks = 6;
} else if (step / 5 >= scale.getMinorTickMarkStepHint()) {
minorTicks = 5;
} else if (step / 4 >= scale.getMinorTickMarkStepHint()) {
minorTicks = 4;
} else {
minorTicks = 2;
}
}
double minorStepInPixel = majorStepInPixel / minorTicks;
double p = ticks.get(0).getPosition();
if (p > 0) {
p -= majorStepInPixel;
for (int i = 1; i < minorTicks; i++) {
int q = (int) Math.floor(p + i * minorStepInPixel);
if (q >= 0 && q < end)
minorPositions.add(q);
}
}
for (int j = 0; j < jmax; j++) {
p = ticks.get(j).getPosition();
for (int i = 1; i < minorTicks; i++) {
int q = (int) Math.floor(p + i * minorStepInPixel);
if (q >= 0 && q < end)
minorPositions.add(q);
}
}
}
}
/**
* @param isTicksIndexBased if true, make ticks based on axis dataset indexes
*/
public void setTicksIndexBased(boolean isTicksIndexBased) {
ticksIndexBased = isTicksIndexBased;
}
}