File indexing completed on 2024-05-19 03:48:55
0001 /* 0002 File : Range.h 0003 Project : LabPlot 0004 Description : basic data range class 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2020-2022 Stefan Gerlach <stefan.gerlach@uni.kn> 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 0010 #ifndef RANGE_H 0011 #define RANGE_H 0012 0013 #include "backend/gsl/parser.h" 0014 #include "backend/nsl/nsl_math.h" 0015 #include "macros.h" //const auto numberLocale = QLocale(); 0016 0017 #include <KLocalizedString> 0018 0019 #include <QDateTime> 0020 #include <QStringList> 0021 0022 #include <cmath> 0023 0024 class QString; 0025 0026 //! Auxiliary class for a data range 0027 /** 0028 * This class represents a data range [start, end] where start can be > end. 0029 * It replaces the Interval class and has following attributes: 0030 * * start 0031 * * end 0032 * * format (Numeric or Datetime) 0033 * * datetime format ("YY-MM-DD ...") 0034 * * scale (Linear, Log, ...) 0035 * 0036 * Only types supporting comparison are supported 0037 */ 0038 class RangeT { // access enum without template 0039 Q_GADGET 0040 public: 0041 enum class Format { Numeric, DateTime }; 0042 Q_ENUM(Format) 0043 // see https://www.originlab.com/doc/Origin-Help/AxesRef-Scale#Type 0044 // TODO: InverseOffset, Prob, Probit, Logit, Weibull 0045 enum class Scale { Linear, Log10, Log2, Ln, Sqrt, Square, Inverse }; 0046 Q_ENUM(Scale) 0047 // static const QStringList& scaleNames(); // see Range.cpp 0048 // TODO: when we have C++17: use inline initialization 0049 static inline QStringList scaleNames{i18n("Linear"), i18n("Log10"), i18n("Log2"), i18n("Ln"), i18n("Sqrt"), i18n("Square")}; 0050 static bool isLogScale(Scale scale) { 0051 if (scale == Scale::Log10 || scale == Scale::Log2 || scale == Scale::Ln) 0052 return true; 0053 return false; 0054 } 0055 }; 0056 0057 template<class T> 0058 class Range : RangeT { 0059 public: 0060 Range() { 0061 } 0062 Range(T start, T end, Format format = Format::Numeric, Scale scale = Scale::Linear) { 0063 this->setRange(start, end, format, scale); 0064 } 0065 // start and end as localized strings like "pi + 1.5" (will be parsed) 0066 Range(const QString& start, const QString& end, const Format format = Format::Numeric, const Scale scale = Scale::Linear) { 0067 const auto numberLocale = QLocale(); 0068 // min 0069 double min = parse(qPrintable(start.simplified()), qPrintable(numberLocale.name())); 0070 if (parse_errors() > 0) // if parsing fails, try default locale 0071 min = parse(qPrintable(start.simplified()), "en_US"); 0072 if (parse_errors() > 0) 0073 min = 0; 0074 0075 // max 0076 double max = parse(qPrintable(end.simplified()), qPrintable(numberLocale.name())); 0077 if (parse_errors() > 0) // if parsing fails, try default locale 0078 max = parse(qPrintable(end.simplified()), "en_US"); 0079 if (parse_errors() > 0) 0080 max = 1.; 0081 0082 // TODO: check for NAN, INF? 0083 this->setRange(min, max, format, scale); 0084 } 0085 0086 T start() const { 0087 return m_start; 0088 } 0089 T end() const { 0090 return m_end; 0091 } 0092 T& start() { 0093 return m_start; 0094 } 0095 T& end() { 0096 return m_end; 0097 } 0098 Format format() const { 0099 return m_format; 0100 } 0101 Format& format() { 0102 return m_format; 0103 } 0104 Scale scale() const { 0105 return m_scale; 0106 } 0107 Scale& scale() { 0108 return m_scale; 0109 } 0110 QString dateTimeFormat() const { 0111 return m_dateTimeFormat; 0112 } 0113 const QString& dateTimeFormat() { 0114 return m_dateTimeFormat; 0115 } 0116 bool autoScale() const { 0117 return m_autoScale; 0118 } 0119 0120 void setStart(T start) { 0121 m_start = start; 0122 } 0123 void setEnd(T end) { 0124 m_end = end; 0125 } 0126 void setRange(T start, T end, Format format, Scale scale) { 0127 m_start = start; 0128 m_end = end; 0129 m_format = format; 0130 m_scale = scale; 0131 } 0132 void setRange(T start, T end) { // set range keeping format and scale 0133 m_start = start; 0134 m_end = end; 0135 } 0136 void setFormat(Format format) { 0137 m_format = format; 0138 } 0139 void setScale(Scale scale) { 0140 m_scale = scale; 0141 } 0142 void setDateTimeFormat(const QString& format) { 0143 m_dateTimeFormat = format; 0144 } 0145 void setAutoScale(bool b) { 0146 m_autoScale = b; 0147 } 0148 0149 T size() const { 0150 return m_end - m_start; 0151 } 0152 T length() const { 0153 return qAbs(m_end - m_start); 0154 } 0155 T center() const { 0156 switch (m_scale) { 0157 case Scale::Linear: 0158 return (m_start + m_end) / 2.; 0159 case Scale::Log10: 0160 return std::pow(10., log10(m_end * m_start) / 2.); 0161 case Scale::Log2: 0162 return std::pow(2., log2(m_end * m_start) / 2.); 0163 case Scale::Ln: 0164 return std::exp(log(m_end * m_start) / 2.); 0165 case Scale::Sqrt: 0166 return std::pow((std::sqrt(m_end) + std::sqrt(m_start)) / 2., 2.); 0167 case Scale::Square: 0168 return std::sqrt((std::pow(m_end, 2.) + std::pow(m_start, 2.)) / 2.); 0169 case Scale::Inverse: 0170 return 1. / ((1. / m_end + 1. / m_start) / 2.); 0171 } 0172 return T(); 0173 } 0174 // calculate step size from number of steps 0175 T stepSize(const int steps) const { 0176 return (steps > 1) ? size() / static_cast<T>(steps - 1) : 0; 0177 } 0178 bool isZero() const { 0179 return (m_end == m_start); 0180 } 0181 bool valid() const { 0182 return (!qIsNaN(m_start) && !qIsNaN(m_end)); 0183 } 0184 bool finite() const { 0185 return (qIsFinite(m_start) && qIsFinite(m_end)); 0186 } 0187 bool inverse() const { 0188 return (m_start > m_end); 0189 } 0190 int direction() const { 0191 return m_start <= m_end ? 1 : -1; 0192 } 0193 // relative precision (high when interval is small compared to range). At least 4 digits 0194 int relativePrecision() const { 0195 return qMax(4, -nsl_math_rounded_decimals(std::abs(m_start) / length()) + 1); 0196 } 0197 bool contains(const Range<T>& other) const { 0198 return (qMin(m_start, m_end) <= qMin(other.start(), other.end()) && qMax(m_start, m_end) >= qMax(other.start(), other.end())); 0199 } 0200 bool contains(T value) const { 0201 return (qMin(m_start, m_end) <= value && qMax(m_start, m_end) >= value); 0202 } 0203 void translate(T offset) { 0204 m_start += offset; 0205 m_end += offset; 0206 } 0207 void extend(T value) { 0208 m_start -= value; 0209 m_end += value; 0210 } 0211 bool operator==(const Range<T>& other) const { 0212 return (m_start == other.start() && m_end == other.end() && m_format == other.format() && m_scale == other.scale()); 0213 } 0214 bool operator!=(const Range<T>& other) const { 0215 return (m_start != other.start() || m_end != other.end() || m_format != other.format() || m_scale != other.scale()); 0216 } 0217 Range<T>& operator+=(const T value) { 0218 m_start += value; 0219 m_end += value; 0220 return *this; 0221 } 0222 Range<T>& operator*=(const T value) { 0223 m_start *= value; 0224 m_end *= value; 0225 return *this; 0226 } 0227 0228 //! Return a string in the format 'start .. end' and uses system locale (specialization see below) 0229 // round == true rounds the double values 0230 QString toString(bool round = true, QLocale locale = QLocale()) const { 0231 Q_UNUSED(round) 0232 if (m_format == Format::Numeric) 0233 return locale.toString(m_start) + QStringLiteral(" .. ") + locale.toString(m_end); 0234 else 0235 return QDateTime::fromMSecsSinceEpoch(m_start, Qt::UTC).toString(m_dateTimeFormat) + QStringLiteral(" .. ") 0236 + QDateTime::fromMSecsSinceEpoch(m_end, Qt::UTC).toString(m_dateTimeFormat); 0237 } 0238 std::string toStdString(bool round = true) const { 0239 return STDSTRING(toString(round)); 0240 } 0241 //! Return a string in the format 'start .. end' and uses number locale 0242 QString toLocaleString(bool round = true) const { 0243 return this->toString(round, QLocale()); 0244 } 0245 0246 // fix start and end when not supported by scale 0247 void fixLimits() { 0248 switch (m_scale) { 0249 case Scale::Linear: // all values are allowed (catch inf, nan?) 0250 break; 0251 case Scale::Log10: 0252 if (m_end <= 0) 0253 m_end = 10.; 0254 if (m_start <= 0) 0255 m_start = m_end / 10.; 0256 break; 0257 case Scale::Log2: 0258 if (m_end <= 0) 0259 m_end = 2.; 0260 if (m_start <= 0) 0261 m_start = m_end / 2.; 0262 break; 0263 case Scale::Ln: 0264 if (m_end <= 0) 0265 m_end = M_E; 0266 if (m_start <= 0) 0267 m_start = m_end / M_E; 0268 break; 0269 case Scale::Sqrt: 0270 case Scale::Square: 0271 case Scale::Inverse: 0272 if (m_end <= 0) // end must be > 0 for start to be < end 0273 m_end = 1.; 0274 if (m_start < 0) 0275 m_start = 0.; 0276 break; 0277 } 0278 } 0279 0280 // extend/shrink range to nice numbers (used in auto scaling) 0281 // get nice size to extend to (see Glassner: Graphic Gems) 0282 double niceSize(double size, bool round) { 0283 const double exponent = std::floor(log10(size)); 0284 const double fraction = size / std::pow(10., exponent); 0285 0286 double niceFraction; // nice (rounded) fraction 0287 if (round) { 0288 if (fraction < 1.5) 0289 niceFraction = 1; 0290 else if (fraction <= 2.5) 0291 niceFraction = 2; 0292 else if (fraction < 7) 0293 niceFraction = 5; 0294 else 0295 niceFraction = 10; 0296 } else { 0297 if (fraction <= 1) 0298 niceFraction = 1; 0299 else if (fraction <= 2) 0300 niceFraction = 2; 0301 else if (fraction <= 5) 0302 niceFraction = 5; 0303 else 0304 niceFraction = 10; 0305 } 0306 DEBUG(Q_FUNC_INFO << ", round = " << round << ", fraction = " << fraction); 0307 DEBUG(Q_FUNC_INFO << ", nice fraction = " << niceFraction); 0308 0309 return niceFraction * std::pow(10., exponent); 0310 } 0311 void niceShrink() { 0312 niceExtend(false); 0313 } 0314 void niceExtend(bool extend = true) { // extend == false means shrink 0315 if (length() == 0) 0316 return; 0317 0318 if (isLogScale(scale()) && (m_start <= 0 || m_end <= 0)) 0319 return; 0320 0321 double base = 10.; 0322 double oldSize = size(); 0323 switch (scale()) { 0324 case Scale::Linear: 0325 case Scale::Log10: 0326 break; // default base 0327 case Scale::Log2: 0328 base = 2.; 0329 break; 0330 case Scale::Ln: 0331 base = M_E; 0332 break; 0333 case Scale::Sqrt: 0334 oldSize = sqrt(oldSize); 0335 break; 0336 case Scale::Square: 0337 oldSize *= oldSize; 0338 break; 0339 case Scale::Inverse: 0340 oldSize = 1. / oldSize; 0341 } 0342 0343 if (isLogScale(scale())) { 0344 if ((extend && m_start < m_end) || (!extend && m_start > m_end)) { 0345 m_start = nsl_math_round_basex(m_start, -1, base); 0346 m_end = nsl_math_round_basex(m_end, -1, base) * base; 0347 } else { 0348 m_start = nsl_math_round_basex(m_start, -1, base) * base; 0349 m_end = nsl_math_round_basex(m_end, -1, base); 0350 } 0351 return; // log scales end here 0352 } 0353 0354 DEBUG(Q_FUNC_INFO << ", scale = " << (int)scale() << ", old size = " << oldSize) 0355 0356 const double newSize = niceSize(oldSize, false); 0357 DEBUG(Q_FUNC_INFO << ", new size = " << newSize); 0358 const double maxTicks = 10; // TODO: parameter? 0359 const double spacing = niceSize(newSize / (maxTicks - 1), true); 0360 DEBUG("spacing = " << spacing) 0361 0362 // extend/shrink range 0363 double new_start = m_start, new_end = m_end; 0364 switch (scale()) { 0365 case Scale::Linear: 0366 case Scale::Log10: // log-scales already done 0367 case Scale::Log2: 0368 case Scale::Ln: 0369 break; 0370 case Scale::Sqrt: 0371 if (m_start < 0 || m_end < 0) 0372 return; 0373 new_start = sqrt(m_start); 0374 new_end = sqrt(m_end); 0375 break; 0376 case Scale::Square: 0377 new_start = m_start * m_start; 0378 new_end = m_end * m_end; 0379 break; 0380 case Scale::Inverse: 0381 if (m_start == 0 || m_end == 0) 0382 return; 0383 new_start = 1. / m_start; 0384 new_end = 1. / m_end; 0385 } 0386 0387 if ((extend && m_start < m_end) || (!extend && m_start > m_end)) { 0388 new_start = std::floor(new_start / spacing) * spacing; 0389 new_end = std::ceil(new_end / spacing) * spacing; 0390 } else { 0391 new_start = std::ceil(new_start / spacing) * spacing; 0392 new_end = std::floor(new_end / spacing) * spacing; 0393 } 0394 DEBUG(" tmp new range: " << new_start << " .. " << new_end) 0395 0396 switch (scale()) { 0397 case Scale::Linear: 0398 case Scale::Log10: // log-scales already done 0399 case Scale::Log2: 0400 case Scale::Ln: 0401 break; 0402 case Scale::Sqrt: 0403 new_start *= new_start; 0404 new_end *= new_end; 0405 break; 0406 case Scale::Square: 0407 if (new_start < 0 || new_end < 0) 0408 return; 0409 new_start = sqrt(new_start); 0410 new_end = sqrt(new_end); 0411 break; 0412 case Scale::Inverse: 0413 if (new_start == 0 || new_end == 0) 0414 return; 0415 new_start = 1. / new_start; 0416 new_end = 1. / new_end; 0417 } 0418 0419 if (std::abs(new_end - new_start) == 0) // avoid empty range 0420 return; 0421 0422 m_start = new_start; 0423 m_end = new_end; 0424 DEBUG(Q_FUNC_INFO << ", new range: " << toStdString()) 0425 } 0426 int autoTickCount() const { 0427 QDEBUG(Q_FUNC_INFO << ", scale = " << scale()) 0428 0429 if (length() == 0) 0430 return 0; 0431 0432 if (isLogScale(scale()) && (m_start <= 0 || m_end <= 0)) 0433 return 1; // datetime test expects value > 0 0434 0435 double order = 0.; 0436 switch (scale()) { 0437 case Scale::Linear: 0438 case Scale::Sqrt: 0439 case Scale::Square: 0440 case Scale::Inverse: 0441 break; 0442 case Scale::Log10: 0443 order = log10(m_end) - log10(m_start); 0444 break; 0445 case Scale::Log2: 0446 order = log2(m_end) - log2(m_start); 0447 break; 0448 case Scale::Ln: 0449 order = log(m_end) - log(m_start); 0450 break; 0451 } 0452 0453 if (isLogScale(scale())) { 0454 DEBUG(Q_FUNC_INFO << ", order = " << std::round(order)) 0455 if (order >= 0) 0456 return std::round(order) + 1; 0457 else // reverse range 0458 return -std::round(order) + 1; 0459 } // log scales end here 0460 0461 DEBUG(Q_FUNC_INFO << ", range = " << toStdString() << ", length() = " << length()) 0462 order = pow(10.0, std::floor(log10(length()))); 0463 0464 DEBUG(Q_FUNC_INFO << ", order of magnitude = " << order) 0465 const int factor = qRound(100 * length() / order); 0466 DEBUG(Q_FUNC_INFO << ", factor = " << factor) 0467 0468 // set number of ticks for certain multiple of small numbers 0469 if (factor % 30 == 0) 0470 return 3 + 1; 0471 if (factor % 40 == 0) 0472 return 4 + 1; 0473 if (factor % 70 == 0) 0474 return 7 + 1; 0475 if (factor % 50 == 0) 0476 return 5 + 1; 0477 if (factor % 90 == 0) 0478 return 9 + 1; 0479 if (factor % 175 == 0) 0480 return 7 + 1; 0481 if (factor % 25 == 0) 0482 return 5 + 1; 0483 if (factor % 105 == 0) 0484 return 7 + 1; 0485 if (factor % 115 == 0) 0486 return 5 + 1; 0487 0488 return 11 + 1; 0489 } 0490 // TODO: touches(), merge(), subtract(), split(), etc. (see Interval) 0491 0492 /*! 0493 * TODO: implement zooming depending on the relZoomPosScene also for non linear scales! 0494 */ 0495 void zoom(const double factor, const bool nice, const double relZoomPosScene = 0.5) { 0496 const double start{this->start()}, end{this->end()}; 0497 switch (scale()) { 0498 case RangeT::Scale::Linear: { 0499 if (relZoomPosScene == 0.5 || nice) 0500 extend(size() * (factor - 1.) / 2.); 0501 else { 0502 // zoom and shift in one step 0503 // Example: Example 1 Example 2 Example 3 0504 // Start 0 0 4.5 0505 // End 10 10 9.5 0506 // factor: 0.5 0.5 2 0507 // relZoomPosScene: 0.9 0.5 0.9 0508 // Expected new start: 2.5 0 0509 // Expected new end: 7.5 10 0510 const double pos = start + (end - start) * relZoomPosScene; // Number at pos scene 9 5 9 0511 this->start() += (pos - start) * (1 - factor); // 4.5 2.5 0 0512 this->end() -= (end - pos) * (1 - factor); // 9.5 7.5 relZoomPos at number 0.9 (fine) 0.5 (fine) (Number 0513 // at pos scene - start) / (end - start) 0514 } 0515 break; 0516 } 0517 case RangeT::Scale::Log10: { 0518 if (start == 0 || end / start <= 0) 0519 break; 0520 const double diff = log10(end / start) * (factor - 1.); 0521 const double extend = pow(10, diff / 2.); 0522 this->end() *= extend; 0523 this->start() /= extend; 0524 break; 0525 } 0526 case RangeT::Scale::Log2: { 0527 if (start == 0 || end / start <= 0) 0528 break; 0529 const double diff = log2(end / start) * (factor - 1.); 0530 const double extend = exp2(diff / 2.); 0531 this->end() *= extend; 0532 this->start() /= extend; 0533 break; 0534 } 0535 case RangeT::Scale::Ln: { 0536 if (start == 0 || end / start <= 0) 0537 break; 0538 const double diff = log(end / start) * (factor - 1.); 0539 const double extend = exp(diff / 2.); 0540 this->end() *= extend; 0541 this->start() /= extend; 0542 break; 0543 } 0544 case RangeT::Scale::Sqrt: { 0545 if (start < 0 || end < 0) 0546 break; 0547 const double diff = (sqrt(end) - sqrt(start)) * (factor - 1.); 0548 extend(diff * diff / 4.); 0549 break; 0550 } 0551 case RangeT::Scale::Square: { 0552 const double diff = (end * end - start * start) * (factor - 1.); 0553 extend(sqrt(std::abs(diff / 2.))); 0554 break; 0555 } 0556 case RangeT::Scale::Inverse: { 0557 const double diff = (1. / start - 1. / end) * (factor - 1.); 0558 extend(1. / std::abs(diff / 2.)); 0559 break; 0560 } 0561 } 0562 0563 // make nice again 0564 if (nice) { 0565 if (factor < 1) // zoomIn 0566 niceShrink(); 0567 else 0568 niceExtend(); 0569 } 0570 } 0571 0572 private: 0573 T m_start{0}; // start value 0574 T m_end{1}; // end value 0575 Format m_format{Format::Numeric}; // format (Numeric or DateTime) 0576 QString m_dateTimeFormat{QLatin1String("yyyy-MM-dd hh:mm:ss")}; // only used for DateTime 0577 Scale m_scale{Scale::Linear}; // scale (Linear, Log , ...) 0578 bool m_autoScale{true}; // auto adapt start and end to all curves using this range (in plot) 0579 }; 0580 0581 // specialization 0582 template<> 0583 inline QString Range<double>::toString(bool round, QLocale locale) const { 0584 if (m_format == Format::Numeric) { 0585 if (round) { 0586 const int relPrec = relativePrecision(); 0587 // DEBUG(Q_FUNC_INFO << ", rel prec = " << relPrec) 0588 return locale.toString(nsl_math_round_precision(m_start, relPrec), 'g', relPrec) + QLatin1String(" .. ") 0589 + locale.toString(nsl_math_round_precision(m_end, relPrec), 'g', relPrec); 0590 } else 0591 return locale.toString(m_start, 'g', 12) + QLatin1String(" .. ") + locale.toString(m_end, 'g', 12); 0592 } else 0593 return QDateTime::fromMSecsSinceEpoch(m_start, Qt::UTC).toString(m_dateTimeFormat) + QLatin1String(" .. ") 0594 + QDateTime::fromMSecsSinceEpoch(m_end, Qt::UTC).toString(m_dateTimeFormat); 0595 } 0596 0597 #endif