Chris@1407: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@1407: Chris@1407: /* Chris@1407: Sonic Visualiser Chris@1407: An audio file viewer and annotation editor. Chris@1407: Centre for Digital Music, Queen Mary, University of London. Chris@1407: This file copyright 2006-2017 Chris Cannam and QMUL. Chris@1407: Chris@1407: This program is free software; you can redistribute it and/or Chris@1407: modify it under the terms of the GNU General Public License as Chris@1407: published by the Free Software Foundation; either version 2 of the Chris@1407: License, or (at your option) any later version. See the file Chris@1407: COPYING included with this distribution for more information. Chris@1407: */ Chris@1407: Chris@1407: #ifndef SV_SCALE_TICK_INTERVALS_H Chris@1407: #define SV_SCALE_TICK_INTERVALS_H Chris@1407: Chris@1407: #include Chris@1407: #include Chris@1407: #include Chris@1407: Chris@1419: #include "LogRange.h" Chris@1419: #include "Debug.h" Chris@1411: Chris@1419: // Can't have this on by default, as we're called on every refresh Chris@1470: //#define DEBUG_SCALE_TICK_INTERVALS 1 Chris@1414: Chris@1407: class ScaleTickIntervals Chris@1407: { Chris@1407: public: Chris@1407: struct Range { Chris@1429: double min; // start of value range Chris@1429: double max; // end of value range Chris@1429: int n; // number of divisions (approximate only) Chris@1407: }; Chris@1407: Chris@1407: struct Tick { Chris@1429: double value; // value this tick represents Chris@1429: std::string label; // value as written Chris@1407: }; Chris@1407: Chris@1417: typedef std::vector Ticks; Chris@1418: Chris@1418: /** Chris@1418: * Return a set of ticks that divide the range r linearly into Chris@1418: * roughly r.n equal divisions, in such a way as to yield Chris@1418: * reasonably human-readable labels. Chris@1418: */ Chris@1407: static Ticks linear(Range r) { Chris@1414: return linearTicks(r); Chris@1414: } Chris@1407: Chris@1418: /** Chris@1418: * Return a set of ticks that divide the range r into roughly r.n Chris@1418: * logarithmic divisions, in such a way as to yield reasonably Chris@1418: * human-readable labels. Chris@1418: */ Chris@1414: static Ticks logarithmic(Range r) { Chris@1418: LogRange::mapRange(r.min, r.max); Chris@1418: return logarithmicAlready(r); Chris@1418: } Chris@1418: Chris@1418: /** Chris@1418: * Return a set of ticks that divide the range r into roughly r.n Chris@1418: * logarithmic divisions, on the asssumption that r.min and r.max Chris@1418: * already represent the logarithms of the boundary values rather Chris@1418: * than the values themselves. Chris@1418: */ Chris@1418: static Ticks logarithmicAlready(Range r) { Chris@1414: return logTicks(r); Chris@1414: } Chris@1418: Chris@1414: private: Chris@1418: enum Display { Chris@1418: Fixed, Chris@1418: Scientific, Chris@1418: Auto Chris@1418: }; Chris@1418: Chris@1417: struct Instruction { Chris@1429: double initial; // value of first tick Chris@1417: double limit; // max from original range Chris@1429: double spacing; // increment between ticks Chris@1429: double roundTo; // what all displayed values should be rounded to Chris@1460: // (if 0.0, then calculate based on precision) Chris@1429: Display display; // whether to use fixed precision (%e, %f, or %g) Chris@1429: int precision; // number of dp (%f) or sf (%e) Chris@1417: bool logUnmap; // true if values represent logs of display values Chris@1417: }; Chris@1417: Chris@1417: static Instruction linearInstruction(Range r) Chris@1414: { Chris@1418: Display display = Auto; Chris@1418: Chris@1429: if (r.max < r.min) { Chris@1429: return linearInstruction({ r.max, r.min, r.n }); Chris@1429: } Chris@1429: if (r.n < 1 || r.max == r.min) { Chris@1418: return { r.min, r.min, 1.0, r.min, display, 1, false }; Chris@1418: } Chris@1429: Chris@1429: double inc = (r.max - r.min) / r.n; Chris@1408: Chris@1408: double digInc = log10(inc); Chris@1408: double digMax = log10(fabs(r.max)); Chris@1408: double digMin = log10(fabs(r.min)); Chris@1408: Chris@1418: int precInc = int(floor(digInc)); Chris@1429: double roundTo = pow(10.0, precInc); Chris@1408: Chris@1408: if (precInc > -4 && precInc < 4) { Chris@1418: display = Fixed; Chris@1418: } else if ((digMax >= -2.0 && digMax <= 3.0) && Chris@1408: (digMin >= -3.0 && digMin <= 3.0)) { Chris@1418: display = Fixed; Chris@1418: } else { Chris@1418: display = Scientific; Chris@1408: } Chris@1408: Chris@1408: int precRange = int(ceil(digMax - digInc)); Chris@1408: Chris@1408: int prec = 1; Chris@1408: Chris@1418: if (display == Fixed) { Chris@1415: if (digInc < 0) { Chris@1410: prec = -precInc; Chris@1415: } else { Chris@1410: prec = 0; Chris@1408: } Chris@1408: } else { Chris@1408: prec = precRange; Chris@1408: } Chris@1408: Chris@1411: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl Chris@1419: << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max Chris@1419: << ", n = " << r.n << ", inc = " << inc << endl; Chris@1470: SVDEBUG << "ScaleTickIntervals: digMax = " << digMax Chris@1419: << ", digInc = " << digInc << endl; Chris@1470: SVDEBUG << "ScaleTickIntervals: display = " << display Chris@1419: << ", inc = " << inc << ", precInc = " << precInc Chris@1419: << ", precRange = " << precRange Chris@1419: << ", prec = " << prec << ", roundTo = " << roundTo Chris@1419: << endl; Chris@1411: #endif Chris@1418: Chris@1418: double min = r.min; Chris@1408: Chris@1418: if (roundTo != 0.0) { Chris@1421: // Round inc to the nearest multiple of roundTo, and min Chris@1421: // to the next multiple of roundTo up. The small offset of Chris@1421: // eps is included to avoid inc of 2.49999999999 rounding Chris@1421: // to 2 or a min of -0.9999999999 rounding to 0, both of Chris@1421: // which would prevent some of our test cases from getting Chris@1421: // the most natural results. Chris@1467: double eps = 1e-7; Chris@1421: inc = round(inc / roundTo + eps) * roundTo; Chris@1418: if (inc < roundTo) inc = roundTo; Chris@1421: min = ceil(min / roundTo - eps) * roundTo; Chris@1418: if (min > r.max) min = r.max; Chris@1421: if (min == -0.0) min = 0.0; Chris@1421: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals: rounded inc to " << inc Chris@1421: << " and min to " << min << endl; Chris@1421: #endif Chris@1418: } Chris@1407: Chris@1418: if (display == Scientific && min != 0.0) { Chris@1413: double digNewMin = log10(fabs(min)); Chris@1413: if (digNewMin < digInc) { Chris@1413: prec = int(ceil(digMax - digNewMin)); Chris@1413: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals: min is smaller than increment, adjusting prec to " << prec << endl; Chris@1413: #endif Chris@1413: } Chris@1413: } Chris@1413: Chris@1418: return { min, r.max, inc, roundTo, display, prec, false }; Chris@1418: } Chris@1418: Chris@1418: static Instruction logInstruction(Range r) Chris@1418: { Chris@1418: Display display = Auto; Chris@1418: Chris@1459: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals::logInstruction: Range is " Chris@1459: << r.min << " to " << r.max << endl; Chris@1459: #endif Chris@1459: Chris@1429: if (r.n < 1) { Chris@1429: return {}; Chris@1429: } Chris@1429: if (r.max < r.min) { Chris@1429: return logInstruction({ r.max, r.min, r.n }); Chris@1429: } Chris@1418: if (r.max == r.min) { Chris@1418: return { r.min, r.max, 1.0, r.min, display, 1, true }; Chris@1418: } Chris@1429: Chris@1429: double inc = (r.max - r.min) / r.n; Chris@1418: Chris@1460: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals::logInstruction: " Chris@1460: << "Naive increment is " << inc << endl; Chris@1460: #endif Chris@1460: Chris@1460: int precision = 1; Chris@1460: Chris@1460: if (inc < 1.0) { Chris@1460: precision = int(ceil(1.0 - inc)) + 1; Chris@1460: } Chris@1460: Chris@1418: double digInc = log10(inc); Chris@1418: int precInc = int(floor(digInc)); Chris@1460: double roundIncTo = pow(10.0, precInc); Chris@1459: Chris@1460: inc = round(inc / roundIncTo) * roundIncTo; Chris@1460: if (inc < roundIncTo) inc = roundIncTo; Chris@1418: Chris@1459: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals::logInstruction: " Chris@1460: << "Rounded increment to " << inc << endl; Chris@1459: #endif Chris@1418: Chris@1418: // if inc is close to giving us powers of two, nudge it Chris@1418: if (fabs(inc - 0.301) < 0.01) { Chris@1418: inc = log10(2.0); Chris@1459: Chris@1459: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals::logInstruction: " Chris@1459: << "Nudged increment to " << inc << " to get powers of two" Chris@1459: << endl; Chris@1459: #endif Chris@1418: } Chris@1418: Chris@1429: double min = r.min; Chris@1418: if (inc != 0.0) { Chris@1418: min = ceil(r.min / inc) * inc; Chris@1418: if (min > r.max) min = r.max; Chris@1418: } Chris@1418: Chris@1460: return { min, r.max, inc, 0.0, display, precision, true }; Chris@1407: } Chris@1407: Chris@1414: static Ticks linearTicks(Range r) { Chris@1417: Instruction instruction = linearInstruction(r); Chris@1417: Ticks ticks = explode(instruction); Chris@1417: return ticks; Chris@1414: } Chris@1414: Chris@1414: static Ticks logTicks(Range r) { Chris@1418: Instruction instruction = logInstruction(r); Chris@1417: Ticks ticks = explode(instruction); Chris@1417: return ticks; Chris@1414: } Chris@1418: Chris@1418: static Tick makeTick(Display display, int precision, double value) { Chris@1459: Chris@1422: if (value == -0.0) { Chris@1422: value = 0.0; Chris@1422: } Chris@1459: Chris@1414: const int buflen = 40; Chris@1414: char buffer[buflen]; Chris@1459: Chris@1459: if (display == Auto) { Chris@1459: Chris@1467: double eps = 1e-7; Chris@1463: Chris@1463: int digits = (value != 0.0 ? Chris@1469: 1 + int(floor(eps + log10(fabs(value)))) : Chris@1463: 0); Chris@1459: Chris@1468: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "makeTick: display = Auto, precision = " Chris@1470: << precision << ", value = " << value Chris@1470: << ", resulting digits = " << digits << endl; Chris@1468: #endif Chris@1468: Chris@1459: // This is not the same logic as %g uses for determining Chris@1459: // whether to delegate to use scientific or fixed notation Chris@1459: Chris@1459: if (digits < -3 || digits > 4) { Chris@1459: Chris@1459: display = Auto; // delegate planning to %g Chris@1459: Chris@1459: } else { Chris@1459: Chris@1459: display = Fixed; Chris@1459: Chris@1459: // in %.*f, the * indicates decimal places, not sig figs Chris@1460: if (precision >= digits) { Chris@1459: precision -= digits; Chris@1459: } else { Chris@1459: precision = 0; Chris@1459: } Chris@1459: } Chris@1459: } Chris@1460: Chris@1459: const char *spec = (display == Auto ? "%.*g" : Chris@1459: display == Scientific ? "%.*e" : Chris@1459: "%.*f"); Chris@1459: Chris@1459: #pragma GCC diagnostic ignored "-Wformat-nonliteral" Chris@1459: Chris@1459: snprintf(buffer, buflen, spec, precision, value); Chris@1459: Chris@1459: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "makeTick: spec = \"" << spec Chris@1459: << "\", prec = " << precision << ", value = " << value Chris@1459: << ", label = \"" << buffer << "\"" << endl; Chris@1459: #endif Chris@1459: Chris@1414: return Tick({ value, std::string(buffer) }); Chris@1414: } Chris@1414: Chris@1417: static Ticks explode(Instruction instruction) { Chris@1417: Chris@1411: #ifdef DEBUG_SCALE_TICK_INTERVALS Chris@1470: SVDEBUG << "ScaleTickIntervals::explode:" << endl Chris@1419: << "initial = " << instruction.initial Chris@1419: << ", limit = " << instruction.limit Chris@1419: << ", spacing = " << instruction.spacing Chris@1419: << ", roundTo = " << instruction.roundTo Chris@1419: << ", display = " << instruction.display Chris@1419: << ", precision = " << instruction.precision Chris@1419: << ", logUnmap = " << instruction.logUnmap Chris@1419: << endl; Chris@1411: #endif Chris@1417: Chris@1417: if (instruction.spacing == 0.0) { Chris@1417: return {}; Chris@1414: } Chris@1417: Chris@1411: double eps = 1e-7; Chris@1417: if (instruction.spacing < eps * 10.0) { Chris@1417: eps = instruction.spacing / 10.0; Chris@1411: } Chris@1417: Chris@1417: double max = instruction.limit; Chris@1412: int n = 0; Chris@1417: Chris@1417: Ticks ticks; Chris@1417: Chris@1412: while (true) { Chris@1460: Chris@1417: double value = instruction.initial + n * instruction.spacing; Chris@1460: Chris@1414: if (value >= max + eps) { Chris@1412: break; Chris@1412: } Chris@1460: Chris@1417: if (instruction.logUnmap) { Chris@1414: value = pow(10.0, value); Chris@1414: } Chris@1460: Chris@1460: double roundTo = instruction.roundTo; Chris@1460: Chris@1460: if (roundTo == 0.0 && value != 0.0) { Chris@1460: // We don't want the internal value secretly not Chris@1460: // matching the displayed one Chris@1460: roundTo = Chris@1469: pow(10, ceil(log10(fabs(value))) - instruction.precision); Chris@1418: } Chris@1460: Chris@1460: if (roundTo != 0.0) { Chris@1460: value = roundTo * round(value / roundTo); Chris@1460: } Chris@1462: Chris@1462: if (fabs(value) < eps) { Chris@1462: value = 0.0; Chris@1462: } Chris@1460: Chris@1429: ticks.push_back(makeTick(instruction.display, Chris@1417: instruction.precision, Chris@1417: value)); Chris@1412: ++n; Chris@1429: } Chris@1417: Chris@1417: return ticks; Chris@1407: } Chris@1407: }; Chris@1407: Chris@1407: #endif