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 <string>
Chris@1407: #include <vector>
Chris@1407: #include <cmath>
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@1419: //#define DEBUG_SCALE_TICK_INTERVALS 1
Chris@1414: 
Chris@1407: class ScaleTickIntervals
Chris@1407: {
Chris@1407: public:
Chris@1407:     struct Range {
Chris@1407: 	double min;        // start of value range
Chris@1407: 	double max;        // end of value range
Chris@1412: 	int n;             // number of divisions (approximate only)
Chris@1407:     };
Chris@1407: 
Chris@1407:     struct Tick {
Chris@1407: 	double value;      // value this tick represents
Chris@1407: 	std::string label; // value as written 
Chris@1407:     };
Chris@1407: 
Chris@1417:     typedef std::vector<Tick> 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@1417: 	double initial;    // value of first tick
Chris@1417:         double limit;      // max from original range
Chris@1417: 	double spacing;    // increment between ticks
Chris@1417: 	double roundTo;    // what all displayed values should be rounded to
Chris@1418: 	Display display;   // whether to use fixed precision (%e, %f, or %g)
Chris@1417: 	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@1407: 	if (r.max < r.min) {
Chris@1417: 	    return linearInstruction({ r.max, r.min, r.n });
Chris@1407: 	}
Chris@1418: 	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@1407: 	
Chris@1407: 	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@1418: 	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@1419:         SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl
Chris@1419:                 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
Chris@1419:                 << ", n = " << r.n << ", inc = " << inc << endl;
Chris@1419:         SVDEBUG << "ScaleTickIntervals: digMax = " << digMax
Chris@1419:                 << ", digInc = " << digInc << endl;
Chris@1419:         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@1418:             inc = round(inc / roundTo) * roundTo;
Chris@1418:             if (inc < roundTo) inc = roundTo;
Chris@1418:             min = ceil(min / roundTo) * roundTo;
Chris@1418:             if (min > r.max) min = r.max;
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@1419:                 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@1418: 	if (r.n < 1) {
Chris@1418: 	    return {};
Chris@1418: 	}
Chris@1418: 	if (r.max < r.min) {
Chris@1418: 	    return logInstruction({ r.max, r.min, r.n });
Chris@1418: 	}
Chris@1418:         if (r.max == r.min) {
Chris@1418:             return { r.min, r.max, 1.0, r.min, display, 1, true };
Chris@1418:         }
Chris@1418: 	
Chris@1418: 	double inc = (r.max - r.min) / r.n;
Chris@1418: 
Chris@1418:         double digInc = log10(inc);
Chris@1418:         int precInc = int(floor(digInc));
Chris@1418: 	double roundTo = pow(10.0, precInc);
Chris@1418: 
Chris@1418:         if (roundTo != 0.0) {
Chris@1418:             inc = round(inc / roundTo) * roundTo;
Chris@1418:             if (inc < roundTo) inc = roundTo;
Chris@1418:         }
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@1418:         }
Chris@1418: 
Chris@1418:         // smallest increment as displayed
Chris@1418:         double minDispInc =
Chris@1418:             LogRange::unmap(r.min + inc) - LogRange::unmap(r.min);
Chris@1418: 
Chris@1418:         int prec = 1;
Chris@1418: 
Chris@1418:         if (minDispInc > 0.0) {
Chris@1418:             prec = int(floor(log10(minDispInc)));
Chris@1418:             if (prec < 0) prec = -prec;
Chris@1418:         }
Chris@1418: 
Chris@1418:         if (r.max >= -2.0 && r.max <= 3.0 &&
Chris@1418:             r.min >= -3.0 && r.min <= 3.0) {
Chris@1418:             display = Fixed;
Chris@1418:             if (prec == 0) prec = 1;
Chris@1418:         }
Chris@1419: 
Chris@1418: #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1419:         SVDEBUG << "ScaleTickIntervals: calculating logInstruction" << endl
Chris@1419:                 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
Chris@1419:                 << ", n = " << r.n << ", inc = " << inc
Chris@1419:                 << ", minDispInc = " << minDispInc << ", digInc = " << digInc
Chris@1419:                 << endl;
Chris@1419:         SVDEBUG << "ScaleTickIntervals: display = " << display
Chris@1419:                 << ", inc = " << inc << ", precInc = " << precInc
Chris@1419:                 << ", prec = " << prec << endl;
Chris@1419:         SVDEBUG << "ScaleTickIntervals: roundTo = " << roundTo << endl;
Chris@1418: #endif
Chris@1418:         
Chris@1418: 	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@1418:         return { min, r.max, inc, 0.0, display, prec, 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@1414:         const int buflen = 40;
Chris@1414:         char buffer[buflen];
Chris@1414:         snprintf(buffer, buflen,
Chris@1418:                  display == Auto ? "%.*g" :
Chris@1418:                  display == Fixed ? "%.*f" :
Chris@1418:                  "%.*e",
Chris@1414:                  precision, value);
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@1419: 	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@1417:             double value = instruction.initial + n * instruction.spacing;
Chris@1414:             if (value >= max + eps) {
Chris@1412:                 break;
Chris@1412:             }
Chris@1417:             if (instruction.logUnmap) {
Chris@1414:                 value = pow(10.0, value);
Chris@1414:             }
Chris@1418:             if (instruction.roundTo != 0.0) {
Chris@1418:                 value = instruction.roundTo * round(value / instruction.roundTo);
Chris@1418:             }
Chris@1418: 	    ticks.push_back(makeTick(instruction.display,
Chris@1417:                                      instruction.precision,
Chris@1417:                                      value));
Chris@1412:             ++n;
Chris@1407: 	}
Chris@1417: 
Chris@1417:         return ticks;
Chris@1407:     }
Chris@1407: };
Chris@1407: 
Chris@1407: #endif