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@1470
|
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@1460
|
86 // (if 0.0, then calculate based on precision)
|
Chris@1429
|
87 Display display; // whether to use fixed precision (%e, %f, or %g)
|
Chris@1429
|
88 int precision; // number of dp (%f) or sf (%e)
|
Chris@1417
|
89 bool logUnmap; // true if values represent logs of display values
|
Chris@1417
|
90 };
|
Chris@1417
|
91
|
Chris@1417
|
92 static Instruction linearInstruction(Range r)
|
Chris@1414
|
93 {
|
Chris@1418
|
94 Display display = Auto;
|
Chris@1418
|
95
|
Chris@1429
|
96 if (r.max < r.min) {
|
Chris@1429
|
97 return linearInstruction({ r.max, r.min, r.n });
|
Chris@1429
|
98 }
|
Chris@1429
|
99 if (r.n < 1 || r.max == r.min) {
|
Chris@1418
|
100 return { r.min, r.min, 1.0, r.min, display, 1, false };
|
Chris@1418
|
101 }
|
Chris@1429
|
102
|
Chris@1429
|
103 double inc = (r.max - r.min) / r.n;
|
Chris@1408
|
104
|
Chris@1408
|
105 double digInc = log10(inc);
|
Chris@1408
|
106 double digMax = log10(fabs(r.max));
|
Chris@1408
|
107 double digMin = log10(fabs(r.min));
|
Chris@1408
|
108
|
Chris@1418
|
109 int precInc = int(floor(digInc));
|
Chris@1429
|
110 double roundTo = pow(10.0, precInc);
|
Chris@1408
|
111
|
Chris@1408
|
112 if (precInc > -4 && precInc < 4) {
|
Chris@1418
|
113 display = Fixed;
|
Chris@1418
|
114 } else if ((digMax >= -2.0 && digMax <= 3.0) &&
|
Chris@1408
|
115 (digMin >= -3.0 && digMin <= 3.0)) {
|
Chris@1418
|
116 display = Fixed;
|
Chris@1418
|
117 } else {
|
Chris@1418
|
118 display = Scientific;
|
Chris@1408
|
119 }
|
Chris@1408
|
120
|
Chris@1408
|
121 int precRange = int(ceil(digMax - digInc));
|
Chris@1408
|
122
|
Chris@1408
|
123 int prec = 1;
|
Chris@1408
|
124
|
Chris@1418
|
125 if (display == Fixed) {
|
Chris@1415
|
126 if (digInc < 0) {
|
Chris@1410
|
127 prec = -precInc;
|
Chris@1415
|
128 } else {
|
Chris@1410
|
129 prec = 0;
|
Chris@1408
|
130 }
|
Chris@1408
|
131 } else {
|
Chris@1408
|
132 prec = precRange;
|
Chris@1408
|
133 }
|
Chris@1408
|
134
|
Chris@1411
|
135 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
136 SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl
|
Chris@1419
|
137 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
|
Chris@1419
|
138 << ", n = " << r.n << ", inc = " << inc << endl;
|
Chris@1470
|
139 SVDEBUG << "ScaleTickIntervals: digMax = " << digMax
|
Chris@1419
|
140 << ", digInc = " << digInc << endl;
|
Chris@1470
|
141 SVDEBUG << "ScaleTickIntervals: display = " << display
|
Chris@1419
|
142 << ", inc = " << inc << ", precInc = " << precInc
|
Chris@1419
|
143 << ", precRange = " << precRange
|
Chris@1419
|
144 << ", prec = " << prec << ", roundTo = " << roundTo
|
Chris@1419
|
145 << endl;
|
Chris@1411
|
146 #endif
|
Chris@1418
|
147
|
Chris@1418
|
148 double min = r.min;
|
Chris@1408
|
149
|
Chris@1418
|
150 if (roundTo != 0.0) {
|
Chris@1421
|
151 // Round inc to the nearest multiple of roundTo, and min
|
Chris@1421
|
152 // to the next multiple of roundTo up. The small offset of
|
Chris@1421
|
153 // eps is included to avoid inc of 2.49999999999 rounding
|
Chris@1421
|
154 // to 2 or a min of -0.9999999999 rounding to 0, both of
|
Chris@1421
|
155 // which would prevent some of our test cases from getting
|
Chris@1421
|
156 // the most natural results.
|
Chris@1467
|
157 double eps = 1e-7;
|
Chris@1421
|
158 inc = round(inc / roundTo + eps) * roundTo;
|
Chris@1418
|
159 if (inc < roundTo) inc = roundTo;
|
Chris@1421
|
160 min = ceil(min / roundTo - eps) * roundTo;
|
Chris@1418
|
161 if (min > r.max) min = r.max;
|
Chris@1421
|
162 if (min == -0.0) min = 0.0;
|
Chris@1421
|
163 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
164 SVDEBUG << "ScaleTickIntervals: rounded inc to " << inc
|
Chris@1421
|
165 << " and min to " << min << endl;
|
Chris@1421
|
166 #endif
|
Chris@1418
|
167 }
|
Chris@1407
|
168
|
Chris@1418
|
169 if (display == Scientific && min != 0.0) {
|
Chris@1413
|
170 double digNewMin = log10(fabs(min));
|
Chris@1413
|
171 if (digNewMin < digInc) {
|
Chris@1413
|
172 prec = int(ceil(digMax - digNewMin));
|
Chris@1413
|
173 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
174 SVDEBUG << "ScaleTickIntervals: min is smaller than increment, adjusting prec to " << prec << endl;
|
Chris@1413
|
175 #endif
|
Chris@1413
|
176 }
|
Chris@1413
|
177 }
|
Chris@1413
|
178
|
Chris@1418
|
179 return { min, r.max, inc, roundTo, display, prec, false };
|
Chris@1418
|
180 }
|
Chris@1418
|
181
|
Chris@1418
|
182 static Instruction logInstruction(Range r)
|
Chris@1418
|
183 {
|
Chris@1418
|
184 Display display = Auto;
|
Chris@1418
|
185
|
Chris@1459
|
186 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
187 SVDEBUG << "ScaleTickIntervals::logInstruction: Range is "
|
Chris@1459
|
188 << r.min << " to " << r.max << endl;
|
Chris@1459
|
189 #endif
|
Chris@1459
|
190
|
Chris@1429
|
191 if (r.n < 1) {
|
Chris@1429
|
192 return {};
|
Chris@1429
|
193 }
|
Chris@1429
|
194 if (r.max < r.min) {
|
Chris@1429
|
195 return logInstruction({ r.max, r.min, r.n });
|
Chris@1429
|
196 }
|
Chris@1418
|
197 if (r.max == r.min) {
|
Chris@1418
|
198 return { r.min, r.max, 1.0, r.min, display, 1, true };
|
Chris@1418
|
199 }
|
Chris@1429
|
200
|
Chris@1429
|
201 double inc = (r.max - r.min) / r.n;
|
Chris@1418
|
202
|
Chris@1460
|
203 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
204 SVDEBUG << "ScaleTickIntervals::logInstruction: "
|
Chris@1460
|
205 << "Naive increment is " << inc << endl;
|
Chris@1460
|
206 #endif
|
Chris@1460
|
207
|
Chris@1460
|
208 int precision = 1;
|
Chris@1460
|
209
|
Chris@1460
|
210 if (inc < 1.0) {
|
Chris@1460
|
211 precision = int(ceil(1.0 - inc)) + 1;
|
Chris@1460
|
212 }
|
Chris@1460
|
213
|
Chris@1418
|
214 double digInc = log10(inc);
|
Chris@1418
|
215 int precInc = int(floor(digInc));
|
Chris@1460
|
216 double roundIncTo = pow(10.0, precInc);
|
Chris@1459
|
217
|
Chris@1460
|
218 inc = round(inc / roundIncTo) * roundIncTo;
|
Chris@1460
|
219 if (inc < roundIncTo) inc = roundIncTo;
|
Chris@1418
|
220
|
Chris@1459
|
221 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
222 SVDEBUG << "ScaleTickIntervals::logInstruction: "
|
Chris@1460
|
223 << "Rounded increment to " << inc << endl;
|
Chris@1459
|
224 #endif
|
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@1470
|
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@1429
|
237 double min = r.min;
|
Chris@1418
|
238 if (inc != 0.0) {
|
Chris@1418
|
239 min = ceil(r.min / inc) * inc;
|
Chris@1418
|
240 if (min > r.max) min = r.max;
|
Chris@1418
|
241 }
|
Chris@1418
|
242
|
Chris@1460
|
243 return { min, r.max, inc, 0.0, display, precision, true };
|
Chris@1407
|
244 }
|
Chris@1407
|
245
|
Chris@1414
|
246 static Ticks linearTicks(Range r) {
|
Chris@1417
|
247 Instruction instruction = linearInstruction(r);
|
Chris@1417
|
248 Ticks ticks = explode(instruction);
|
Chris@1417
|
249 return ticks;
|
Chris@1414
|
250 }
|
Chris@1414
|
251
|
Chris@1414
|
252 static Ticks logTicks(Range r) {
|
Chris@1418
|
253 Instruction instruction = logInstruction(r);
|
Chris@1417
|
254 Ticks ticks = explode(instruction);
|
Chris@1417
|
255 return ticks;
|
Chris@1414
|
256 }
|
Chris@1418
|
257
|
Chris@1418
|
258 static Tick makeTick(Display display, int precision, double value) {
|
Chris@1459
|
259
|
Chris@1422
|
260 if (value == -0.0) {
|
Chris@1422
|
261 value = 0.0;
|
Chris@1422
|
262 }
|
Chris@1459
|
263
|
Chris@1414
|
264 const int buflen = 40;
|
Chris@1414
|
265 char buffer[buflen];
|
Chris@1459
|
266
|
Chris@1459
|
267 if (display == Auto) {
|
Chris@1459
|
268
|
Chris@1467
|
269 double eps = 1e-7;
|
Chris@1463
|
270
|
Chris@1463
|
271 int digits = (value != 0.0 ?
|
Chris@1469
|
272 1 + int(floor(eps + log10(fabs(value)))) :
|
Chris@1463
|
273 0);
|
Chris@1459
|
274
|
Chris@1468
|
275 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
276 SVDEBUG << "makeTick: display = Auto, precision = "
|
Chris@1470
|
277 << precision << ", value = " << value
|
Chris@1470
|
278 << ", resulting digits = " << digits << endl;
|
Chris@1468
|
279 #endif
|
Chris@1468
|
280
|
Chris@1459
|
281 // This is not the same logic as %g uses for determining
|
Chris@1459
|
282 // whether to delegate to use scientific or fixed notation
|
Chris@1459
|
283
|
Chris@1459
|
284 if (digits < -3 || digits > 4) {
|
Chris@1459
|
285
|
Chris@1459
|
286 display = Auto; // delegate planning to %g
|
Chris@1459
|
287
|
Chris@1459
|
288 } else {
|
Chris@1459
|
289
|
Chris@1459
|
290 display = Fixed;
|
Chris@1459
|
291
|
Chris@1459
|
292 // in %.*f, the * indicates decimal places, not sig figs
|
Chris@1460
|
293 if (precision >= digits) {
|
Chris@1459
|
294 precision -= digits;
|
Chris@1459
|
295 } else {
|
Chris@1459
|
296 precision = 0;
|
Chris@1459
|
297 }
|
Chris@1459
|
298 }
|
Chris@1459
|
299 }
|
Chris@1460
|
300
|
Chris@1459
|
301 const char *spec = (display == Auto ? "%.*g" :
|
Chris@1459
|
302 display == Scientific ? "%.*e" :
|
Chris@1459
|
303 "%.*f");
|
Chris@1459
|
304
|
Chris@1459
|
305 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
Chris@1459
|
306
|
Chris@1459
|
307 snprintf(buffer, buflen, spec, precision, value);
|
Chris@1459
|
308
|
Chris@1459
|
309 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
310 SVDEBUG << "makeTick: spec = \"" << spec
|
Chris@1459
|
311 << "\", prec = " << precision << ", value = " << value
|
Chris@1459
|
312 << ", label = \"" << buffer << "\"" << endl;
|
Chris@1459
|
313 #endif
|
Chris@1459
|
314
|
Chris@1414
|
315 return Tick({ value, std::string(buffer) });
|
Chris@1414
|
316 }
|
Chris@1414
|
317
|
Chris@1417
|
318 static Ticks explode(Instruction instruction) {
|
Chris@1417
|
319
|
Chris@1411
|
320 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1470
|
321 SVDEBUG << "ScaleTickIntervals::explode:" << endl
|
Chris@1419
|
322 << "initial = " << instruction.initial
|
Chris@1419
|
323 << ", limit = " << instruction.limit
|
Chris@1419
|
324 << ", spacing = " << instruction.spacing
|
Chris@1419
|
325 << ", roundTo = " << instruction.roundTo
|
Chris@1419
|
326 << ", display = " << instruction.display
|
Chris@1419
|
327 << ", precision = " << instruction.precision
|
Chris@1419
|
328 << ", logUnmap = " << instruction.logUnmap
|
Chris@1419
|
329 << endl;
|
Chris@1411
|
330 #endif
|
Chris@1417
|
331
|
Chris@1417
|
332 if (instruction.spacing == 0.0) {
|
Chris@1417
|
333 return {};
|
Chris@1414
|
334 }
|
Chris@1417
|
335
|
Chris@1411
|
336 double eps = 1e-7;
|
Chris@1417
|
337 if (instruction.spacing < eps * 10.0) {
|
Chris@1417
|
338 eps = instruction.spacing / 10.0;
|
Chris@1411
|
339 }
|
Chris@1417
|
340
|
Chris@1417
|
341 double max = instruction.limit;
|
Chris@1412
|
342 int n = 0;
|
Chris@1417
|
343
|
Chris@1417
|
344 Ticks ticks;
|
Chris@1417
|
345
|
Chris@1412
|
346 while (true) {
|
Chris@1460
|
347
|
Chris@1417
|
348 double value = instruction.initial + n * instruction.spacing;
|
Chris@1460
|
349
|
Chris@1414
|
350 if (value >= max + eps) {
|
Chris@1412
|
351 break;
|
Chris@1412
|
352 }
|
Chris@1460
|
353
|
Chris@1417
|
354 if (instruction.logUnmap) {
|
Chris@1414
|
355 value = pow(10.0, value);
|
Chris@1414
|
356 }
|
Chris@1460
|
357
|
Chris@1460
|
358 double roundTo = instruction.roundTo;
|
Chris@1460
|
359
|
Chris@1460
|
360 if (roundTo == 0.0 && value != 0.0) {
|
Chris@1460
|
361 // We don't want the internal value secretly not
|
Chris@1460
|
362 // matching the displayed one
|
Chris@1460
|
363 roundTo =
|
Chris@1469
|
364 pow(10, ceil(log10(fabs(value))) - instruction.precision);
|
Chris@1418
|
365 }
|
Chris@1460
|
366
|
Chris@1460
|
367 if (roundTo != 0.0) {
|
Chris@1460
|
368 value = roundTo * round(value / roundTo);
|
Chris@1460
|
369 }
|
Chris@1462
|
370
|
Chris@1462
|
371 if (fabs(value) < eps) {
|
Chris@1462
|
372 value = 0.0;
|
Chris@1462
|
373 }
|
Chris@1460
|
374
|
Chris@1429
|
375 ticks.push_back(makeTick(instruction.display,
|
Chris@1417
|
376 instruction.precision,
|
Chris@1417
|
377 value));
|
Chris@1412
|
378 ++n;
|
Chris@1429
|
379 }
|
Chris@1417
|
380
|
Chris@1417
|
381 return ticks;
|
Chris@1407
|
382 }
|
Chris@1407
|
383 };
|
Chris@1407
|
384
|
Chris@1407
|
385 #endif
|