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@1429
|
185 if (r.n < 1) {
|
Chris@1429
|
186 return {};
|
Chris@1429
|
187 }
|
Chris@1429
|
188 if (r.max < r.min) {
|
Chris@1429
|
189 return logInstruction({ r.max, r.min, r.n });
|
Chris@1429
|
190 }
|
Chris@1418
|
191 if (r.max == r.min) {
|
Chris@1418
|
192 return { r.min, r.max, 1.0, r.min, display, 1, true };
|
Chris@1418
|
193 }
|
Chris@1429
|
194
|
Chris@1429
|
195 double inc = (r.max - r.min) / r.n;
|
Chris@1418
|
196
|
Chris@1418
|
197 double digInc = log10(inc);
|
Chris@1418
|
198 int precInc = int(floor(digInc));
|
Chris@1429
|
199 double roundTo = pow(10.0, precInc);
|
Chris@1418
|
200
|
Chris@1418
|
201 if (roundTo != 0.0) {
|
Chris@1418
|
202 inc = round(inc / roundTo) * roundTo;
|
Chris@1418
|
203 if (inc < roundTo) inc = roundTo;
|
Chris@1418
|
204 }
|
Chris@1418
|
205
|
Chris@1418
|
206 // if inc is close to giving us powers of two, nudge it
|
Chris@1418
|
207 if (fabs(inc - 0.301) < 0.01) {
|
Chris@1418
|
208 inc = log10(2.0);
|
Chris@1418
|
209 }
|
Chris@1418
|
210
|
Chris@1418
|
211 // smallest increment as displayed
|
Chris@1418
|
212 double minDispInc =
|
Chris@1418
|
213 LogRange::unmap(r.min + inc) - LogRange::unmap(r.min);
|
Chris@1418
|
214
|
Chris@1418
|
215 int prec = 1;
|
Chris@1418
|
216
|
Chris@1418
|
217 if (minDispInc > 0.0) {
|
Chris@1418
|
218 prec = int(floor(log10(minDispInc)));
|
Chris@1418
|
219 if (prec < 0) prec = -prec;
|
Chris@1418
|
220 }
|
Chris@1418
|
221
|
Chris@1418
|
222 if (r.max >= -2.0 && r.max <= 3.0 &&
|
Chris@1418
|
223 r.min >= -3.0 && r.min <= 3.0) {
|
Chris@1418
|
224 display = Fixed;
|
Chris@1418
|
225 if (prec == 0) prec = 1;
|
Chris@1418
|
226 }
|
Chris@1419
|
227
|
Chris@1418
|
228 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1419
|
229 SVDEBUG << "ScaleTickIntervals: calculating logInstruction" << endl
|
Chris@1419
|
230 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
|
Chris@1419
|
231 << ", n = " << r.n << ", inc = " << inc
|
Chris@1419
|
232 << ", minDispInc = " << minDispInc << ", digInc = " << digInc
|
Chris@1419
|
233 << endl;
|
Chris@1419
|
234 SVDEBUG << "ScaleTickIntervals: display = " << display
|
Chris@1419
|
235 << ", inc = " << inc << ", precInc = " << precInc
|
Chris@1419
|
236 << ", prec = " << prec << endl;
|
Chris@1419
|
237 SVDEBUG << "ScaleTickIntervals: roundTo = " << roundTo << endl;
|
Chris@1418
|
238 #endif
|
Chris@1418
|
239
|
Chris@1429
|
240 double min = r.min;
|
Chris@1418
|
241 if (inc != 0.0) {
|
Chris@1418
|
242 min = ceil(r.min / inc) * inc;
|
Chris@1418
|
243 if (min > r.max) min = r.max;
|
Chris@1418
|
244 }
|
Chris@1418
|
245
|
Chris@1418
|
246 return { min, r.max, inc, 0.0, display, prec, true };
|
Chris@1407
|
247 }
|
Chris@1407
|
248
|
Chris@1414
|
249 static Ticks linearTicks(Range r) {
|
Chris@1417
|
250 Instruction instruction = linearInstruction(r);
|
Chris@1417
|
251 Ticks ticks = explode(instruction);
|
Chris@1417
|
252 return ticks;
|
Chris@1414
|
253 }
|
Chris@1414
|
254
|
Chris@1414
|
255 static Ticks logTicks(Range r) {
|
Chris@1418
|
256 Instruction instruction = logInstruction(r);
|
Chris@1417
|
257 Ticks ticks = explode(instruction);
|
Chris@1417
|
258 return ticks;
|
Chris@1414
|
259 }
|
Chris@1418
|
260
|
Chris@1418
|
261 static Tick makeTick(Display display, int precision, double value) {
|
Chris@1422
|
262 if (value == -0.0) {
|
Chris@1422
|
263 value = 0.0;
|
Chris@1422
|
264 }
|
Chris@1414
|
265 const int buflen = 40;
|
Chris@1414
|
266 char buffer[buflen];
|
Chris@1414
|
267 snprintf(buffer, buflen,
|
Chris@1418
|
268 display == Auto ? "%.*g" :
|
Chris@1418
|
269 display == Fixed ? "%.*f" :
|
Chris@1418
|
270 "%.*e",
|
Chris@1414
|
271 precision, value);
|
Chris@1414
|
272 return Tick({ value, std::string(buffer) });
|
Chris@1414
|
273 }
|
Chris@1414
|
274
|
Chris@1417
|
275 static Ticks explode(Instruction instruction) {
|
Chris@1417
|
276
|
Chris@1411
|
277 #ifdef DEBUG_SCALE_TICK_INTERVALS
|
Chris@1429
|
278 SVDEBUG << "ScaleTickIntervals::explode:" << endl
|
Chris@1419
|
279 << "initial = " << instruction.initial
|
Chris@1419
|
280 << ", limit = " << instruction.limit
|
Chris@1419
|
281 << ", spacing = " << instruction.spacing
|
Chris@1419
|
282 << ", roundTo = " << instruction.roundTo
|
Chris@1419
|
283 << ", display = " << instruction.display
|
Chris@1419
|
284 << ", precision = " << instruction.precision
|
Chris@1419
|
285 << ", logUnmap = " << instruction.logUnmap
|
Chris@1419
|
286 << endl;
|
Chris@1411
|
287 #endif
|
Chris@1417
|
288
|
Chris@1417
|
289 if (instruction.spacing == 0.0) {
|
Chris@1417
|
290 return {};
|
Chris@1414
|
291 }
|
Chris@1417
|
292
|
Chris@1411
|
293 double eps = 1e-7;
|
Chris@1417
|
294 if (instruction.spacing < eps * 10.0) {
|
Chris@1417
|
295 eps = instruction.spacing / 10.0;
|
Chris@1411
|
296 }
|
Chris@1417
|
297
|
Chris@1417
|
298 double max = instruction.limit;
|
Chris@1412
|
299 int n = 0;
|
Chris@1417
|
300
|
Chris@1417
|
301 Ticks ticks;
|
Chris@1417
|
302
|
Chris@1412
|
303 while (true) {
|
Chris@1417
|
304 double value = instruction.initial + n * instruction.spacing;
|
Chris@1414
|
305 if (value >= max + eps) {
|
Chris@1412
|
306 break;
|
Chris@1412
|
307 }
|
Chris@1417
|
308 if (instruction.logUnmap) {
|
Chris@1414
|
309 value = pow(10.0, value);
|
Chris@1414
|
310 }
|
Chris@1418
|
311 if (instruction.roundTo != 0.0) {
|
Chris@1418
|
312 value = instruction.roundTo * round(value / instruction.roundTo);
|
Chris@1418
|
313 }
|
Chris@1429
|
314 ticks.push_back(makeTick(instruction.display,
|
Chris@1417
|
315 instruction.precision,
|
Chris@1417
|
316 value));
|
Chris@1412
|
317 ++n;
|
Chris@1429
|
318 }
|
Chris@1417
|
319
|
Chris@1417
|
320 return ticks;
|
Chris@1407
|
321 }
|
Chris@1407
|
322 };
|
Chris@1407
|
323
|
Chris@1407
|
324 #endif
|