comparison 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
comparison
equal deleted inserted replaced
1324:d4a28d1479a8 1527:710e6250a401
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 */
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 */
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 */
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
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
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
246 static Ticks linearTicks(Range r) {
247 Instruction instruction = linearInstruction(r);
248 Ticks ticks = explode(instruction);
249 return ticks;
250 }
251
252 static Ticks logTicks(Range r) {
253 Instruction instruction = logInstruction(r);
254 Ticks ticks = explode(instruction);
255 return ticks;
256 }
257
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
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