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