diff base/ScaleTickIntervals.h @ 1527:710e6250a401 zoom

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