1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ 2 3 /* 4 Sonic Visualiser 5 An audio file viewer and annotation editor. 6 Centre for Digital Music, Queen Mary, University of London. 7 This file copyright 2006-2017 Chris Cannam and QMUL. 8 9 This program is free software; you can redistribute it and/or 10 modify it under the terms of the GNU General Public License as 11 published by the Free Software Foundation; either version 2 of the 12 License, or (at your option) any later version. See the file 13 COPYING included with this distribution for more information. 14 */ 15 16 #ifndef SV_SCALE_TICK_INTERVALS_H 17 #define SV_SCALE_TICK_INTERVALS_H 18 19 #include <string> 20 #include <vector> 21 #include <cmath> 22 23 #include "LogRange.h" 24 #include "Debug.h" 25 26 // Can't have this on by default, as we're called on every refresh 27 //#define DEBUG_SCALE_TICK_INTERVALS 1 28 29 class ScaleTickIntervals 30 { 31 public: 32 struct Range { 33 double min; // start of value range 34 double max; // end of value range 35 int n; // number of divisions (approximate only) 36 }; 37 38 struct Tick { 39 double value; // value this tick represents 40 std::string label; // value as written 41 }; 42 43 typedef std::vector<Tick> Ticks; 44 45 /** 46 * Return a set of ticks that divide the range r linearly into 47 * roughly r.n equal divisions, in such a way as to yield 48 * reasonably human-readable labels. 49 */ linear(Range r)50 static Ticks linear(Range r) { 51 return linearTicks(r); 52 } 53 54 /** 55 * Return a set of ticks that divide the range r into roughly r.n 56 * logarithmic divisions, in such a way as to yield reasonably 57 * human-readable labels. 58 */ logarithmic(Range r)59 static Ticks logarithmic(Range r) { 60 LogRange::mapRange(r.min, r.max); 61 return logarithmicAlready(r); 62 } 63 64 /** 65 * Return a set of ticks that divide the range r into roughly r.n 66 * logarithmic divisions, on the asssumption that r.min and r.max 67 * already represent the logarithms of the boundary values rather 68 * than the values themselves. 69 */ logarithmicAlready(Range r)70 static Ticks logarithmicAlready(Range r) { 71 return logTicks(r); 72 } 73 74 private: 75 enum Display { 76 Fixed, 77 Scientific, 78 Auto 79 }; 80 81 struct Instruction { 82 double initial; // value of first tick 83 double limit; // max from original range 84 double spacing; // increment between ticks 85 double roundTo; // what all displayed values should be rounded to 86 // (if 0.0, then calculate based on precision) 87 Display display; // whether to use fixed precision (%e, %f, or %g) 88 int precision; // number of dp (%f) or sf (%e) 89 bool logUnmap; // true if values represent logs of display values 90 }; 91 linearInstruction(Range r)92 static Instruction linearInstruction(Range r) 93 { 94 Display display = Auto; 95 96 if (r.max < r.min) { 97 return linearInstruction({ r.max, r.min, r.n }); 98 } 99 if (r.n < 1 || r.max == r.min) { 100 return { r.min, r.min, 1.0, r.min, display, 1, false }; 101 } 102 103 double inc = (r.max - r.min) / r.n; 104 105 double digInc = log10(inc); 106 double digMax = log10(fabs(r.max)); 107 double digMin = log10(fabs(r.min)); 108 109 int precInc = int(floor(digInc)); 110 double roundTo = pow(10.0, precInc); 111 112 if (precInc > -4 && precInc < 4) { 113 display = Fixed; 114 } else if ((digMax >= -2.0 && digMax <= 3.0) && 115 (digMin >= -3.0 && digMin <= 3.0)) { 116 display = Fixed; 117 } else { 118 display = Scientific; 119 } 120 121 int precRange = int(ceil(digMax - digInc)); 122 123 int prec = 1; 124 125 if (display == Fixed) { 126 if (digInc < 0) { 127 prec = -precInc; 128 } else { 129 prec = 0; 130 } 131 } else { 132 prec = precRange; 133 } 134 135 #ifdef DEBUG_SCALE_TICK_INTERVALS 136 SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl 137 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max 138 << ", n = " << r.n << ", inc = " << inc << endl; 139 SVDEBUG << "ScaleTickIntervals: digMax = " << digMax 140 << ", digInc = " << digInc << endl; 141 SVDEBUG << "ScaleTickIntervals: display = " << display 142 << ", inc = " << inc << ", precInc = " << precInc 143 << ", precRange = " << precRange 144 << ", prec = " << prec << ", roundTo = " << roundTo 145 << endl; 146 #endif 147 148 double min = r.min; 149 150 if (roundTo != 0.0) { 151 // Round inc to the nearest multiple of roundTo, and min 152 // to the next multiple of roundTo up. The small offset of 153 // eps is included to avoid inc of 2.49999999999 rounding 154 // to 2 or a min of -0.9999999999 rounding to 0, both of 155 // which would prevent some of our test cases from getting 156 // the most natural results. 157 double eps = 1e-7; 158 inc = round(inc / roundTo + eps) * roundTo; 159 if (inc < roundTo) inc = roundTo; 160 min = ceil(min / roundTo - eps) * roundTo; 161 if (min > r.max) min = r.max; 162 if (min == -0.0) min = 0.0; 163 #ifdef DEBUG_SCALE_TICK_INTERVALS 164 SVDEBUG << "ScaleTickIntervals: rounded inc to " << inc 165 << " and min to " << min << endl; 166 #endif 167 } 168 169 if (display == Scientific && min != 0.0) { 170 double digNewMin = log10(fabs(min)); 171 if (digNewMin < digInc) { 172 prec = int(ceil(digMax - digNewMin)); 173 #ifdef DEBUG_SCALE_TICK_INTERVALS 174 SVDEBUG << "ScaleTickIntervals: min is smaller than increment, adjusting prec to " << prec << endl; 175 #endif 176 } 177 } 178 179 return { min, r.max, inc, roundTo, display, prec, false }; 180 } 181 logInstruction(Range r)182 static Instruction logInstruction(Range r) 183 { 184 Display display = Auto; 185 186 #ifdef DEBUG_SCALE_TICK_INTERVALS 187 SVDEBUG << "ScaleTickIntervals::logInstruction: Range is " 188 << r.min << " to " << r.max << endl; 189 #endif 190 191 if (r.n < 1) { 192 return {}; 193 } 194 if (r.max < r.min) { 195 return logInstruction({ r.max, r.min, r.n }); 196 } 197 if (r.max == r.min) { 198 return { r.min, r.max, 1.0, r.min, display, 1, true }; 199 } 200 201 double inc = (r.max - r.min) / r.n; 202 203 #ifdef DEBUG_SCALE_TICK_INTERVALS 204 SVDEBUG << "ScaleTickIntervals::logInstruction: " 205 << "Naive increment is " << inc << endl; 206 #endif 207 208 int precision = 1; 209 210 if (inc < 1.0) { 211 precision = int(ceil(1.0 - inc)) + 1; 212 } 213 214 double digInc = log10(inc); 215 int precInc = int(floor(digInc)); 216 double roundIncTo = pow(10.0, precInc); 217 218 inc = round(inc / roundIncTo) * roundIncTo; 219 if (inc < roundIncTo) inc = roundIncTo; 220 221 #ifdef DEBUG_SCALE_TICK_INTERVALS 222 SVDEBUG << "ScaleTickIntervals::logInstruction: " 223 << "Rounded increment to " << inc << endl; 224 #endif 225 226 // if inc is close to giving us powers of two, nudge it 227 if (fabs(inc - 0.301) < 0.01) { 228 inc = log10(2.0); 229 230 #ifdef DEBUG_SCALE_TICK_INTERVALS 231 SVDEBUG << "ScaleTickIntervals::logInstruction: " 232 << "Nudged increment to " << inc << " to get powers of two" 233 << endl; 234 #endif 235 } 236 237 double min = r.min; 238 if (inc != 0.0) { 239 min = ceil(r.min / inc) * inc; 240 if (min > r.max) min = r.max; 241 } 242 243 return { min, r.max, inc, 0.0, display, precision, true }; 244 } 245 linearTicks(Range r)246 static Ticks linearTicks(Range r) { 247 Instruction instruction = linearInstruction(r); 248 Ticks ticks = explode(instruction); 249 return ticks; 250 } 251 logTicks(Range r)252 static Ticks logTicks(Range r) { 253 Instruction instruction = logInstruction(r); 254 Ticks ticks = explode(instruction); 255 return ticks; 256 } 257 makeTick(Display display,int precision,double value)258 static Tick makeTick(Display display, int precision, double value) { 259 260 if (value == -0.0) { 261 value = 0.0; 262 } 263 264 const int buflen = 40; 265 char buffer[buflen]; 266 267 if (display == Auto) { 268 269 double eps = 1e-7; 270 271 int digits = (value != 0.0 ? 272 1 + int(floor(eps + log10(fabs(value)))) : 273 0); 274 275 #ifdef DEBUG_SCALE_TICK_INTERVALS 276 SVDEBUG << "makeTick: display = Auto, precision = " 277 << precision << ", value = " << value 278 << ", resulting digits = " << digits << endl; 279 #endif 280 281 // This is not the same logic as %g uses for determining 282 // whether to delegate to use scientific or fixed notation 283 284 if (digits < -3 || digits > 4) { 285 286 display = Auto; // delegate planning to %g 287 288 } else { 289 290 display = Fixed; 291 292 // in %.*f, the * indicates decimal places, not sig figs 293 if (precision >= digits) { 294 precision -= digits; 295 } else { 296 precision = 0; 297 } 298 } 299 } 300 301 const char *spec = (display == Auto ? "%.*g" : 302 display == Scientific ? "%.*e" : 303 "%.*f"); 304 305 #pragma GCC diagnostic ignored "-Wformat-nonliteral" 306 307 snprintf(buffer, buflen, spec, precision, value); 308 309 #ifdef DEBUG_SCALE_TICK_INTERVALS 310 SVDEBUG << "makeTick: spec = \"" << spec 311 << "\", prec = " << precision << ", value = " << value 312 << ", label = \"" << buffer << "\"" << endl; 313 #endif 314 315 return Tick({ value, std::string(buffer) }); 316 } 317 explode(Instruction instruction)318 static Ticks explode(Instruction instruction) { 319 320 #ifdef DEBUG_SCALE_TICK_INTERVALS 321 SVDEBUG << "ScaleTickIntervals::explode:" << endl 322 << "initial = " << instruction.initial 323 << ", limit = " << instruction.limit 324 << ", spacing = " << instruction.spacing 325 << ", roundTo = " << instruction.roundTo 326 << ", display = " << instruction.display 327 << ", precision = " << instruction.precision 328 << ", logUnmap = " << instruction.logUnmap 329 << endl; 330 #endif 331 332 if (instruction.spacing == 0.0) { 333 return {}; 334 } 335 336 double eps = 1e-7; 337 if (instruction.spacing < eps * 10.0) { 338 eps = instruction.spacing / 10.0; 339 } 340 341 double max = instruction.limit; 342 int n = 0; 343 344 Ticks ticks; 345 346 while (true) { 347 348 double value = instruction.initial + n * instruction.spacing; 349 350 if (value >= max + eps) { 351 break; 352 } 353 354 if (instruction.logUnmap) { 355 value = pow(10.0, value); 356 } 357 358 double roundTo = instruction.roundTo; 359 360 if (roundTo == 0.0 && value != 0.0) { 361 // We don't want the internal value secretly not 362 // matching the displayed one 363 roundTo = 364 pow(10, ceil(log10(fabs(value))) - instruction.precision); 365 } 366 367 if (roundTo != 0.0) { 368 value = roundTo * round(value / roundTo); 369 } 370 371 if (fabs(value) < eps) { 372 value = 0.0; 373 } 374 375 ticks.push_back(makeTick(instruction.display, 376 instruction.precision, 377 value)); 378 ++n; 379 } 380 381 return ticks; 382 } 383 }; 384 385 #endif 386