package org.archstudio.bna.things.graphs; import static com.google.common.base.Preconditions.checkArgument; import java.text.DecimalFormat; import java.util.Iterator; import org.archstudio.swtutils.constants.LineStyle; import org.archstudio.sysutils.CloneableObject; import org.archstudio.sysutils.SystemUtils; import com.google.common.base.Function; import com.google.common.collect.AbstractIterator; public class NumericAxis implements CloneableObject { public static enum UnitType { LINEAR(true, false), LOGARITHMIC(false, true), LOGARITHMIC_SUBDIVIDED(false, true); private final boolean linear; private final boolean logarithmic; private UnitType(boolean linear, boolean logarithmic) { this.linear = linear; this.logarithmic = logarithmic; } public boolean isLinear() { return linear; } public boolean isLogarithmic() { return logarithmic; } } public class Range { public final double min; public final double max; private Range(double min, double max) { this.min = min; this.max = max; } @Override public String toString() { return "[" + min + "," + max + "]"; } } public static class Value { public final double actualValue; public final double linearOffset; public Value(double actualValue, double linearOffset) { this.actualValue = actualValue; this.linearOffset = linearOffset; } @Override public String toString() { return actualValue + "(" + linearOffset + ")"; } } public UnitType type = UnitType.LINEAR; public double min = Double.NaN; // NaN indicates automatic public double max = Double.NaN; // NaN indicates automatic public double unit = 10; public Function<Double, String> formatter = new Function<Double, String>() { DecimalFormat formatter = new DecimalFormat("###,###.###"); @Override public String apply(Double input) { return formatter.format(input); } }; public LineStyle lineStyle = LineStyle.SOLID; public int angle = 0; public boolean reverse = false; public NumericAxis() { super(); } @Override public Object clone() { try { NumericAxis clone = (NumericAxis) super.clone(); return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } public Value toValue(double value, Range range) { checkArgument(range.min <= value && value <= range.max, "value (%s) must be within range (%s)", value, range); switch (type) { case LINEAR: if (!reverse) { return new Value(value, value - range.min); } else { return new Value(value, range.max - value); } case LOGARITHMIC: case LOGARITHMIC_SUBDIVIDED: checkArgument(value > 0, "value (%s) must be > 0", value); checkArgument(unit > 1, "unit (%s) must be > 1", unit); if (!reverse) { return new Value(value, (Math.log(value) - Math.log(range.min)) / Math.log(unit) * unit); } else { return new Value(value, (Math.log(range.max) - Math.log(value)) / Math.log(unit) * unit); } } throw new IllegalArgumentException(type.toString()); } public Range getAxisRange(double minValue, double maxValue) { checkArgument(minValue <= maxValue, "min value (%s) must be <= max value (%s)", minValue, maxValue); switch (type) { case LINEAR: checkArgument(unit > 0, "unit (%s) must be > 0", unit); break; case LOGARITHMIC: case LOGARITHMIC_SUBDIVIDED: checkArgument(unit > 1, "unit (%s) must be > 1", unit); break; } double min = this.min; if (Double.isNaN(min)) { switch (type) { case LINEAR: min = Math.floor(minValue / unit) * unit; break; case LOGARITHMIC: case LOGARITHMIC_SUBDIVIDED: checkArgument(minValue > 0, "min (%s) must be positive", minValue); min = Math.pow(unit, Math.floor(Math.log(minValue) / Math.log(unit))); break; } } double max = this.max; if (Double.isNaN(max)) { switch (type) { case LINEAR: max = Math.ceil(maxValue / unit) * unit; break; case LOGARITHMIC: case LOGARITHMIC_SUBDIVIDED: checkArgument(maxValue > 0, "max (%s) must be positive", maxValue); max = Math.pow(unit, Math.ceil(Math.log(maxValue) / Math.log(unit))); break; } } checkArgument(min <= max, "min value (%s) must be <= max value (%s)", min, max); return new Range(min, max); } public Iterable<Value> getAxisValues(final Range range) { final double unit = this.unit; checkArgument(range.min <= range.max, "min value (%s) must be <= max value (%s)", range.min, range.max); switch (type) { case LINEAR: { checkArgument(unit > 0, "unit (%s) must be greater than 0", unit); return new Iterable<Value>() { @Override public Iterator<Value> iterator() { return new AbstractIterator<Value>() { long offset = 0; long count = SystemUtils.round(Math.floor((range.max - range.min) / unit)); @Override protected Value computeNext() { if (offset > count) { return endOfData(); } return toValue(range.min + offset++ * unit, range); } }; } }; } case LOGARITHMIC: { checkArgument(unit > 1, "unit (%s) must be > 1", unit); checkArgument(range.min > 0, "min (%s) must be > 0", range.min); checkArgument(range.max > 0, "max (%s) must be > 0", range.max); return new Iterable<Value>() { @Override public Iterator<Value> iterator() { return new AbstractIterator<Value>() { long offset = 0; long count = SystemUtils.round(Math.log(range.max / range.min) / Math.log(unit)); @Override protected Value computeNext() { if (offset > count) { return endOfData(); } return toValue(range.min * Math.pow(unit, offset++), range); } }; } }; } case LOGARITHMIC_SUBDIVIDED: { checkArgument(unit > 1, "unit (%s) must be > 1", unit); checkArgument(range.min > 0, "min (%s) must be > 0", range.min); checkArgument(range.max > 0, "max (%s) must be > 0", range.max); return new Iterable<Value>() { @Override public Iterator<Value> iterator() { return new AbstractIterator<Value>() { long offset = 0; long count = SystemUtils.round(Math.log(range.max / range.min) / Math.log(unit)); long suboffset = 1; long subcount = SystemUtils.round(Math.floor(unit)); @Override protected Value computeNext() { if (offset >= count) { return endOfData(); } if (suboffset >= subcount) { suboffset = 1; offset++; } return toValue(range.min * Math.pow(unit, offset + Math.log(suboffset++) / Math.log(unit)), range); } }; } }; } } throw new IllegalArgumentException(type.toString()); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + angle; result = prime * result + (formatter == null ? 0 : formatter.hashCode()); result = prime * result + (lineStyle == null ? 0 : lineStyle.hashCode()); long temp; temp = Double.doubleToLongBits(max); result = prime * result + (int) (temp ^ temp >>> 32); temp = Double.doubleToLongBits(min); result = prime * result + (int) (temp ^ temp >>> 32); result = prime * result + (reverse ? 1231 : 1237); result = prime * result + (type == null ? 0 : type.hashCode()); temp = Double.doubleToLongBits(unit); result = prime * result + (int) (temp ^ temp >>> 32); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } NumericAxis other = (NumericAxis) obj; if (angle != other.angle) { return false; } if (formatter == null) { if (other.formatter != null) { return false; } } else if (!formatter.equals(other.formatter)) { return false; } if (lineStyle != other.lineStyle) { return false; } if (Double.doubleToLongBits(max) != Double.doubleToLongBits(other.max)) { return false; } if (Double.doubleToLongBits(min) != Double.doubleToLongBits(other.min)) { return false; } if (reverse != other.reverse) { return false; } if (type != other.type) { return false; } if (Double.doubleToLongBits(unit) != Double.doubleToLongBits(other.unit)) { return false; } return true; } }