package net.sf.openrocket.unit; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import net.sf.openrocket.util.Chars; public class FractionalUnit extends Unit { private final static char FRACTION = Chars.FRACTION; private final static String[] NUMERATOR = { "\u2070", // 0 "\u00B9", // 1 "\u00B2", // 2 "\u00B3", // 3 "\u2074", // 4 "\u2075", // 5 "\u2076", // 6 "\u2077", // 7 "\u2078", // 8 "\u2079" // 9 }; private final static String[] DENOMINATOR = { "\u2080", // 0 "\u2081", // 1 "\u2082", // 2 "\u2083", // 3 "\u2084", // 4 "\u2085", // 5 "\u2086", // 6 "\u2087", // 7 "\u2088", // 8 "\u2089" // 9 }; // This is the base of the fractions. ie, 16d for 1/16ths. private final int fractionBase; // This is 1d/fractionBase; private final double fractionValue; // This is the value used when incrementing/decrementing. private final double incrementValue; // If the actual value differs from the decimal representation by more than this, // we display as decimals. private final double epsilon; private final String unitLabel; public FractionalUnit(double multiplier, String unit, String unitLabel, int fractionBase, double incrementValue) { this(multiplier, unit, unitLabel, fractionBase, incrementValue, 0.1d / fractionBase); } public FractionalUnit(double multiplier, String unit, String unitLabel, int fractionBase, double incrementValue, double epsilon) { super(multiplier, unit); this.unitLabel = unitLabel; this.fractionBase = fractionBase; this.fractionValue = 1.0d / fractionBase; this.incrementValue = incrementValue; this.epsilon = epsilon; } @Override public double round(double value) { return roundTo(value, fractionValue); } private double roundTo(double value, double fraction) { double remainder = Math.IEEEremainder(value, fraction); return value - remainder; } @Override public double getNextValue(double value) { double rounded = roundTo(value, incrementValue); if (rounded <= value + epsilon) { rounded += incrementValue; } return rounded; } @Override public double getPreviousValue(double value) { double rounded = roundTo(value, incrementValue); if (rounded >= value - epsilon) { rounded -= incrementValue; } return rounded; } @Override public Tick[] getTicks(double start, double end, double minor, double major) { // Convert values start = toUnit(start); end = toUnit(end); minor = toUnit(minor); major = toUnit(major); if (minor <= 0 || major <= 0 || major < minor) { throw new IllegalArgumentException("getTicks called with minor=" + minor + " major=" + major); } ArrayList<Tick> ticks = new ArrayList<Tick>(); int mod2, mod3, mod4; // Moduli for minor-notable, major-nonnotable, major-notable double minstep; // Find the smallest possible step size double one = 1; while (one > minor) one /= 2; while (one < minor) one *= 2; minstep = one; mod2 = 16; // Find step size for major ticks one = 1; while (one > major) one /= 10; while (one < major) one *= 10; if (one / 2 >= major) { // major step is round-five, major-notable is next round-ten double majorstep = one / 2; mod3 = (int) Math.round(majorstep / minstep); mod4 = mod3 * 2; } else { // major step is round-ten, major-notable is next round-ten mod3 = (int) Math.round(one / minstep); mod4 = mod3 * 10; } // Check for clashes between minor-notable and major-nonnotable if (mod3 == mod2) { if (mod2 == 2) mod2 = 1; // Every minor tick is notable else mod2 = 5; // Every fifth minor tick is notable } // Calculate starting position int pos = (int) Math.ceil(start / minstep); // System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4); while (pos * minstep <= end) { double unitValue = pos * minstep; double value = fromUnit(unitValue); if (pos % mod4 == 0) ticks.add(new Tick(value, unitValue, true, true)); else if (pos % mod3 == 0) ticks.add(new Tick(value, unitValue, true, false)); else if (pos % mod2 == 0) ticks.add(new Tick(value, unitValue, false, true)); else ticks.add(new Tick(value, unitValue, false, false)); pos++; } return ticks.toArray(new Tick[0]); } @Override public String toString(double value) { double correctVal = toUnit(value); double val = round(correctVal); if (Math.abs(val - correctVal) > epsilon) { NumberFormat decFormat = new DecimalFormat("#.###"); return decFormat.format(correctVal); } NumberFormat intFormat = new DecimalFormat("#"); double sign = Math.signum(val); double posValue = sign * val; double intPart = Math.floor(posValue); double frac = Math.rint((posValue - intPart) / fractionValue); double fracBase = fractionBase; // Reduce fraction. while (frac > 0 && fracBase > 2 && frac % 2 == 0) { frac /= 2.0; fracBase /= 2.0; } posValue *= sign; if (frac == 0.0) { return intFormat.format(posValue); } else if (intPart == 0.0) { return (sign < 0 ? "-" : "") + numeratorString(Double.valueOf(frac).intValue()) + FRACTION + denominatorString(Double.valueOf(fracBase).intValue()); } else { return intFormat.format(sign * intPart) + " " + numeratorString(Double.valueOf(frac).intValue()) + FRACTION + denominatorString(Double.valueOf(fracBase).intValue()); } } private String numeratorString(int value) { String rep = ""; if (value == 0) { return "0"; } while (value > 0) { rep = NUMERATOR[value % 10] + rep; value = value / 10; } return rep; } private String denominatorString(int value) { String rep = ""; if (value == 0) { return "0"; } while (value > 0) { rep = DENOMINATOR[value % 10] + rep; value = value / 10; } return rep; } @Override public String toStringUnit(double value) { if (Double.isNaN(value)) return "N/A"; String s = toString(value); s += " " + unitLabel; return s; } }