/*******************************************************************************
* Copyright (c) 2012, 2017 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.eclipse.nebula.visualization.xygraph.linearscale;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.nebula.visualization.xygraph.figures.DAxis;
import org.eclipse.nebula.visualization.xygraph.linearscale.TickFactory.TickFormatting;
/**
* Class to represent a major tick for axes with scientific notation.
* This tick provider is used if a {@link DAxis} is created.
*
*/
public class LinearScaleTicks2 implements ITicksProvider {
/**
* The name of this tick provider
*/
public static final String NAME = "DIAMOND";
/**
* the list of ticks marks
*/
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;
/** the scale */
protected IScaleProvider scale;
private boolean ticksIndexBased;
/** default: show max label */
private boolean showMaxLabel = true;
/** default: show min label */
private boolean showMinLabel = true;
/**
* constructor
*
* @param scale
*/
public LinearScaleTicks2(DAxis 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) Math.round(t.getPosition()));
return positions;
}
@Override
public List<Boolean> getVisibilities() {
List<Boolean> visibilities = new ArrayList<Boolean>();
for (int i = 0; i < ticks.size(); i++) {
visibilities.add(true);
}
return visibilities;
}
@Override
public List<String> getLabels() {
List<String> labels = new ArrayList<String>();
for (int i = 0; i < ticks.size(); i++) {
labels.add(ticks.get(i).getText());
}
return labels;
}
@Override
public int getPosition(int index) {
return (int) Math.round(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;
}
@Override
public boolean isShowMaxLabel() {
return showMaxLabel;
}
@Override
public void setShowMaxLabel(boolean showMaxLabel) {
this.showMaxLabel = showMaxLabel;
}
@Override
public boolean isShowMinLabel() {
return showMinLabel;
}
@Override
public void setShowMinLabel(boolean showMinLabel) {
this.showMinLabel = showMinLabel;
}
private final static int TICKMINDIST_IN_PIXELS_X = 40;
private final static int TICKMINDIST_IN_PIXELS_Y = 30;
private final static int MAX_TICKS = 12;
private final static int MIN_TICKS = 3;
@Override
public Range update(final double min, final double max, int length) {
if (scale.isLogScaleEnabled() && (min <= 0 || max <= 0))
throw new IllegalArgumentException("Range for log scale must be in positive range");
final int maximumNumTicks = Math.min(MAX_TICKS,
length / (scale.isHorizontal() ? TICKMINDIST_IN_PIXELS_X : TICKMINDIST_IN_PIXELS_Y) + 1);
int numTicks = Math.max(3, maximumNumTicks);
final TickFactory tf;
DAxis aScale = (DAxis) 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);
}
}
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.max(Math.abs(min), Math.abs(max));
double power = mantissa == 0 ? -1 : Math.log10(mantissa);
if (power >= AbstractScale.ENGINEERING_LIMIT || power < -6) {
format = AbstractScale.DEFAULT_ENGINEERING_FORMAT;
} else if (power <= 0) {
StringBuilder form = new StringBuilder("##0.00");
while (power < -1) {
power += 1;
form.append("#");
}
format = form.toString();
} else {
format = "############.##";
}
return format;
}
@Override
public int getHeadMargin() {
if (ticks == null || ticks.size() == 0 || maxWidth == 0 || maxHeight == 0) {
// No ticks yet
final Dimension l = scale.getDimension(scale.getScaleRange().getLower());
if (scale.isHorizontal()) {
// calculate X margin with r
return l.width;
}
// 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) {
// No ticks yet
final Dimension h = scale.getDimension(scale.getScaleRange().getUpper());
if (scale.isHorizontal()) {
// calculate X margin with r
return h.width;
}
// 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;
}
if (length <= 0) {
return true; // sanity check
}
maxWidth = 0;
maxHeight = 0;
final boolean hasNegative = ticks.get(0).getText().startsWith(MINUS);
final int minus = scale.getDimension(MINUS).width;
for (Tick t : ticks) {
final String l = t.getText();
final Dimension d = scale.getDimension(l);
if (hasNegative && !l.startsWith(MINUS)) {
d.width += minus;
}
if (d.width > maxWidth) {
maxWidth = d.width;
}
if (d.height > maxHeight) {
maxHeight = d.height;
}
}
if (isReversed) {
for (Tick t : ticks) {
t.setPosition(length - length * t.getPosition() + hMargin);
}
} else {
for (Tick t : ticks) {
t.setPosition(length * t.getPosition() + hMargin);
}
}
// re-expand length (so labels can flow into margins)
length += hMargin + tMargin;
if (scale.isHorizontal()) {
final int space = (int) (0.67 * scale.getDimension(" ").width);
int last = 0;
for (Tick t : ticks) {
final Dimension d = scale.getDimension(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.getDimension(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;
}
/**
* fraction of major tick step between 9 and 10
*/
private static final double LAST_STEP_FRAC = 1 - Math.log10(9);
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.round(Math.abs(Math.log10(ticks.get(1).getValue() / ticks.get(0).getValue())));
// gap is greater than a decade
if (minorTicks > 10)
return;
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;
}
}