/*******************************************************************************
* Copyright (c) 2017 Diamond Light Source and others.
* 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.figures;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.nebula.visualization.xygraph.linearscale.ITicksProvider;
import org.eclipse.nebula.visualization.xygraph.linearscale.LinearScaleTickLabels;
import org.eclipse.nebula.visualization.xygraph.linearscale.LinearScaleTickLabels2;
import org.eclipse.nebula.visualization.xygraph.linearscale.LinearScaleTickMarks;
import org.eclipse.nebula.visualization.xygraph.linearscale.LinearScaleTickMarks2;
import org.eclipse.nebula.visualization.xygraph.linearscale.Range;
import org.eclipse.swt.widgets.Display;
/**
* The Diamond Light Source implementation of the axis figure.
*
* @author Baha El-Kassaby - Diamond Light Source contributions
*/
public class DAxis extends Axis {
/** if true, then ticks are based on axis dataset indexes */
private boolean ticksIndexBased;
private Map<Integer, Format> cachedFormats = new HashMap<Integer, Format>();
private boolean ticksAtEnds = true;
private boolean forceRange;
/** the user format */
protected boolean userDefinedFormat = false;
private boolean axisAutoscaleTight = false;
/**
* Constructor
*
* @param title
* title of the axis
* @param yAxis
* true if this is the Y-Axis, false if this is the X-Axis.
*/
public DAxis(final String title, final boolean yAxis) {
super(title, yAxis);
}
@Override
protected LinearScaleTickLabels createLinearScaleTickLabels() {
return new LinearScaleTickLabels2(this);
}
@Override
protected LinearScaleTickMarks createLinearScaleTickMarks() {
return new LinearScaleTickMarks2(this);
}
/**
* Calculate span of a textual form of object in scale's orientation
*
* @param obj
* object
* @return span in pixel
*/
public int calculateSpan(Object obj) {
final Dimension extent = getDimension(obj);
if (isHorizontal()) {
return extent.width;
}
return extent.height;
}
@Override
public int getMargin() {
if (isDirty())
setMargin(getTicksProvider().getHeadMargin());
return getMargin(false);
}
/**
* Get scaling for axis in terms of pixels/unit
*
* @return scaling
*/
public double getScaling() {
int length = getLength();
int margin = getMargin();
if (isLogScaleEnabled())
return (Math.log10(max) - Math.log10(min)) / (length - 2 * margin);
return (max - min) / (length - 2 * margin);
}
@Override
protected void layout() {
super.layout();
layoutTicks();
}
protected void layoutTicks() {
updateTick();
Rectangle area = getClientArea();
LinearScaleTickLabels tickLabels = getScaleTickLabels();
LinearScaleTickMarks tickMarks = getScaleTickMarks();
if (isHorizontal()) {
if (getTickLabelSide() == LabelSide.Primary) {
tickLabels.setBounds(
new Rectangle(area.x, area.y + LinearScaleTickMarks.MAJOR_TICK_LENGTH + SPACE_BTW_MARK_LABEL,
area.width, area.height - LinearScaleTickMarks.MAJOR_TICK_LENGTH));
tickMarks.setBounds(area);
} else {
tickLabels.setBounds(new Rectangle(area.x,
area.y + area.height - LinearScaleTickMarks.MAJOR_TICK_LENGTH
- tickLabels.getTickLabelMaxHeight() - SPACE_BTW_MARK_LABEL,
area.width, tickLabels.getTickLabelMaxHeight()));
tickMarks.setBounds(new Rectangle(area.x, area.y + area.height - LinearScaleTickMarks.MAJOR_TICK_LENGTH,
area.width, LinearScaleTickMarks.MAJOR_TICK_LENGTH));
}
} else {
if (getTickLabelSide() == LabelSide.Primary) {
tickLabels.setBounds(new Rectangle(
area.x + area.width - LinearScaleTickMarks.MAJOR_TICK_LENGTH
- tickLabels.getTickLabelMaxLength() - SPACE_BTW_MARK_LABEL,
area.y, tickLabels.getTickLabelMaxLength(), area.height));
tickMarks.setBounds(new Rectangle(
area.x + area.width - LinearScaleTickMarks.MAJOR_TICK_LENGTH - LinearScaleTickMarks.LINE_WIDTH,
area.y, LinearScaleTickMarks.MAJOR_TICK_LENGTH + LinearScaleTickMarks.LINE_WIDTH, area.height));
} else {
tickLabels.setBounds(new Rectangle(area.x + LinearScaleTickMarks.MAJOR_TICK_LENGTH + SPACE_BTW_MARK_LABEL,
area.y, tickLabels.getTickLabelMaxLength(), area.height));
tickMarks.setBounds(new Rectangle(area.x, area.y, LinearScaleTickMarks.MAJOR_TICK_LENGTH, area.height));
}
}
}
/**
* @param isTicksIndexBased
* if true, make ticks based on axis dataset indexes
*/
public void setTicksIndexBased(boolean isTicksIndexBased) {
if (ticksIndexBased != isTicksIndexBased)
((LinearScaleTickLabels2)getScaleTickLabels()).setTicksIndexBased(isTicksIndexBased);
ticksIndexBased = isTicksIndexBased;
}
/**
*
* @return True if ticks are index based
*/
public boolean isTicksIndexBased() {
return ticksIndexBased;
}
@Override
public String format(Object obj) {
return format(obj, 0);
}
@Override
public void updateTick() {
if (isDirty()) {
setLength(isHorizontal() ? getClientArea().width : getClientArea().height);
int length = getLength();
int margin = getMargin();
if (length > 2 * margin) {
Range r = getScaleTickLabels().update(length - 2 * margin);
if (r != null && !r.equals(getRange()) && !forceRange) {
setLocalRange(r);
} else {
setLocalRange(null);
}
}
setDirty(false);
}
}
/**
* Formats the given object as a DateFormat if Date is enabled or as a
* DecimalFormat. This is based on an internal format pattern given the
* object in parameter. When formatting a date, if minOrMaxDate is true as
* well as autoFormat, then the SimpleDateFormat us used to format the
* object.
*
* @param obj
* the object
* @param extraDP
* must be non-negative
* @return the formatted string
*/
public String format(Object obj, int extraDP) {
if (extraDP < 0) {
throw new IllegalArgumentException("Number of extra decimal places must be non-negative");
}
String formatPattern = getFormatPattern();
boolean autoFormat = isAutoFormat();
if (cachedFormats.get(extraDP) == null) {
if (isDateEnabled()) {
if (autoFormat || formatPattern == null || formatPattern.equals("")
|| formatPattern.equals(default_decimal_format)
|| formatPattern.equals(DEFAULT_ENGINEERING_FORMAT)) {
// (?) overridden anyway
formatPattern = DEFAULT_DATE_FORMAT;
int timeUnit = getTimeUnit();
double length = Math.abs(max - min);
// less than a second
if (length <= 1000 || timeUnit == Calendar.MILLISECOND) {
formatPattern = "HH:mm:ss.SSS";
}
// less than a hour
else if (length <= 3600000d || timeUnit == Calendar.SECOND) {
formatPattern = "HH:mm:ss";
}
// less than a day
else if (length <= 86400000d || timeUnit == Calendar.MINUTE) {
formatPattern = "HH:mm";
}
// less than a week
else if (length <= 604800000d || timeUnit == Calendar.HOUR_OF_DAY) {
formatPattern = "dd HH:mm";
}
// less than a month
else if (length <= 2592000000d || timeUnit == Calendar.DATE) {
formatPattern = "MMMMM d";
}
// less than a year
else if (length <= 31536000000d || timeUnit == Calendar.MONTH) {
formatPattern = "yyyy MMMMM";
} else {// if (timeUnit == Calendar.YEAR) {
formatPattern = "yyyy";
}
if (formatPattern == null || formatPattern.equals("")) {
autoFormat = true;
}
}
setFormatPattern(formatPattern);
cachedFormats.put(extraDP, new SimpleDateFormat(formatPattern));
} else {
if (formatPattern == null || formatPattern.isEmpty() || formatPattern.equals(default_decimal_format)
|| formatPattern.equals(DEFAULT_DATE_FORMAT)) {
formatPattern = getAutoFormat(min, max);
setFormatPattern(formatPattern);
if (formatPattern == null || formatPattern.equals("")) {
autoFormat = true;
}
}
String ePattern = formatPattern;
if (extraDP > 0) {
int e = formatPattern.lastIndexOf('E');
StringBuilder temp = new StringBuilder(e == -1 ? formatPattern : formatPattern.substring(0, e));
for (int i = 0; i < extraDP; i++) {
temp.append('#');
}
if (e != -1) {
temp.append(formatPattern.substring(e));
}
ePattern = temp.toString();
}
cachedFormats.put(extraDP, new DecimalFormat(ePattern));
}
internalSetAutoFormat(autoFormat);
}
if (isDateEnabled() && obj instanceof Number) {
return cachedFormats.get(extraDP).format(new Date(((Number) obj).longValue()));
}
return cachedFormats.get(extraDP).format(obj);
}
protected String getAutoFormat(double min, double max) {
ITicksProvider ticks = getTicksProvider();
if (ticks == null) {
if ((max != 0 && Math.abs(Math.log10(Math.abs(max))) >= ENGINEERING_LIMIT)
|| (min != 0 && Math.abs(Math.log10(Math.abs(min))) >= ENGINEERING_LIMIT)) {
return DEFAULT_ENGINEERING_FORMAT;
}
return default_decimal_format;
}
return ticks.getDefaultFormatPattern(min, max);
}
@Override
public void setDateEnabled(boolean dateEnabled) {
super.setDateEnabled(dateEnabled);
cachedFormats.clear();
setDirty(true);
revalidate();
}
@Override
public void setFormatPattern(String formatPattern) {
this.userDefinedFormat = true;
setFormat(formatPattern);
}
private void setFormat(String formatPattern) {
cachedFormats.clear();
super.setFormatPattern(formatPattern);
}
@Override
public void setRange(double lower, double upper) {
Range old_range = getRange();
if (old_range.getLower() == lower && old_range.getUpper() == upper) {
return;
}
setTicksAtEnds(false);
super.setRange(lower, upper);
fireAxisRangeChanged(old_range, getRange());
}
@Override
public void setAutoFormat(boolean autoFormat) {
super.setAutoFormat(autoFormat);
if (autoFormat) {
cachedFormats.clear();
}
}
@Override
public void setLogScale(boolean enabled) throws IllegalStateException {
boolean cur = isLogScaleEnabled();
super.setLogScale(enabled);
final IXYGraph xyGraph = getXyGraph();
if (cur != enabled && xyGraph != null) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
xyGraph.performAutoScale();
xyGraph.getPlotArea().layout();
xyGraph.revalidate();
xyGraph.repaint();
}
});
}
setTicksAtEnds(true);
}
@Override
public boolean performAutoScale(boolean force) {
// Anything to do? Autoscale not enabled nor forced?
if (getTraceList().size() <= 0 || !(force || getAutoScale())) {
return false;
}
// Get range of data in all traces
Range range = getTraceDataRange();
if (range == null) {
return false;
}
double dataMin = range.getLower();
double dataMax = range.getUpper();
// Get current axis range, determine how 'different' they are
double axisMax = getRange().getUpper();
double axisMin = getRange().getLower();
if (rangeIsUnchanged(dataMin, dataMax, axisMin, axisMax) || Double.isInfinite(dataMin)
|| Double.isInfinite(dataMax) || Double.isNaN(dataMin) || Double.isNaN(dataMax)) {
return false;
}
// The threshold is 'shared' between upper and lower range, times by 0.5
final double thr = (axisMax - axisMin) * 0.5 * getAutoScaleThreshold();
boolean lowerChanged = (dataMin - axisMin) < 0 || (dataMin - axisMin) >= thr;
boolean upperChanged = (axisMax - dataMax) < 0 || (axisMax - dataMax) >= thr;
// If both the changes are lower than threshold, return
if (!lowerChanged && !upperChanged) {
return false;
}
// Calculate updated range
double newMax = upperChanged ? dataMax : axisMax;
double newMin = lowerChanged ? dataMin : axisMin;
range = !isInverted() ? new Range(newMin, newMax) : new Range(newMax, newMin);
// by-pass overridden method as it sets ticks to false
super.setRange(range.getLower(), range.getUpper());
fireAxisRangeChanged(getRange(), range);
setTicksAtEnds(!axisAutoscaleTight);
repaint();
return true;
}
/**
* Determines if upper or lower data has changed from current axis limits
*
* @param dataMin
* - min of data in buffer
* @param dataMax
* - max of data in buffer
* @param axisMin
* - current axis min
* @param axisMax
* - current axis max
* @return TRUE if data and axis max and min values are equal
*/
private boolean rangeIsUnchanged(double dataMin, double dataMax, double axisMin, double axisMax) {
return Double.doubleToLongBits(dataMin) == Double.doubleToLongBits(axisMin)
&& Double.doubleToLongBits(dataMax) == Double.doubleToLongBits(axisMax);
}
/**
*
*/
public void clear() {
for (Iterator<IAxisListener> it = listeners.iterator(); it.hasNext();) {
if (getTraceList().contains(it.next()))
it.remove();
}
getTraceList().clear();
}
/**
* @param axisTight
* set whether autoscale sets axis range tight to the data or the
* end of axis is set to the nearest tickmark
*/
public void setAxisAutoscaleTight(boolean axisTight) {
this.axisAutoscaleTight = axisTight;
}
/**
* @return true if autoscaling axis is tight to displayed data
*/
public boolean isAxisAutoscaleTight() {
return this.axisAutoscaleTight;
}
/**
* Sets whether ticks at ends of axis are shown
*
* @param ticksAtEnds
*/
public void setTicksAtEnds(boolean ticksAtEnds) {
this.ticksAtEnds = ticksAtEnds;
}
/**
* Returns true if ticks at end of axis are shown
*/
public boolean hasTicksAtEnds() {
return ticksAtEnds;
}
/**
* Sets whether there is a user defined format or not
*
* @param hasUserDefinedFormat
*/
public void setHasUserDefinedFormat(boolean hasUserDefinedFormat) {
userDefinedFormat = hasUserDefinedFormat;
}
/**
*
* @return true if user format is defined
*/
public boolean hasUserDefinedFormat() {
return userDefinedFormat;
}
}