From 177df72d338d36c69c3bf10ca5795172686a6eba Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Wed, 22 Sep 2021 20:56:13 +0200 Subject: [PATCH] profile: unify formating of axis labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of a host of virtual functions, let the base class (DiveCartesianAxis) do the formatting of the axis labels. To do so, it needs to know how to convert the internal representation (e.g. mm) into the displayed value (e.g. feet). Moreover, this transformation has to be adapted when changing the locale-setting, therefore do it for every plot() call. The transformation itself cannot be a simple linear translation, because we have non-absolute display units, namely °C and °F. Thankfully affine transformations are enough though. Only one custom formatter remains: the time axis. It might be a good idea to remove the virtual function and do this via a flag. This is all done not so much for code simplification, but because for a general layout of the axis labels, the axis has to understand the values of the labels and not only handle them as opaque texts. Signed-off-by: Berthold Stoeger --- profile-widget/divecartesianaxis.cpp | 35 +++++++++++++++++----------- profile-widget/divecartesianaxis.h | 16 ++++++++++--- profile-widget/profilescene.cpp | 30 +++++++++++++++++++++++- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/profile-widget/divecartesianaxis.cpp b/profile-widget/divecartesianaxis.cpp index 1c154bd0d..4689b9278 100644 --- a/profile-widget/divecartesianaxis.cpp +++ b/profile-widget/divecartesianaxis.cpp @@ -22,6 +22,7 @@ DiveCartesianAxis::DiveCartesianAxis(Position position, int integralDigits, int double labelScale, bool printMode, bool isGrayscale, ProfileScene &scene) : printMode(printMode), position(position), + fractionalDigits(fractionalDigits), gridColor(gridColor), scene(scene), orientation(LeftToRight), @@ -32,7 +33,8 @@ DiveCartesianAxis::DiveCartesianAxis(Position position, int integralDigits, int lineVisibility(true), labelScale(labelScale), changed(true), - dpr(dpr) + dpr(dpr), + transform({1.0, 0.0}) { QPen pen; pen.setColor(getColor(TIME_GRID, isGrayscale)); @@ -77,6 +79,13 @@ void DiveCartesianAxis::setOrientation(Orientation o) changed = true; } +void DiveCartesianAxis::setTransform(double a, double b) +{ + transform.a = a; + transform.b = b; + changed = true; +} + QColor DiveCartesianAxis::colorForValue(double) const { return QColor(Qt::black); @@ -282,9 +291,19 @@ void DiveCartesianAxis::animateChangeLine(const QRectF &rectIn, int animSpeed) sizeChanged(); } +double DiveCartesianAxis::Transform::to(double x) const +{ + return a*x + b; +} + +double DiveCartesianAxis::Transform::from(double y) const +{ + return (y - b) / a; +} + QString DiveCartesianAxis::textForValue(double value) const { - return QString("%L1").arg(value, 0, 'g', 4); + return QStringLiteral("%L1").arg(transform.to(value), 0, 'f', fractionalDigits); } void DiveCartesianAxis::setTickInterval(double i) @@ -353,13 +372,6 @@ std::pair DiveCartesianAxis::screenMinMax() const : std::make_pair(rect.top(), rect.bottom()); } -QString DepthAxis::textForValue(double value) const -{ - if (value == 0) - return QString(); - return get_depth_string(lrint(value), false, false); -} - QColor DepthAxis::colorForValue(double) const { return QColor(Qt::red); @@ -389,11 +401,6 @@ void TimeAxis::updateTicks(int animSpeed) } } -QString TemperatureAxis::textForValue(double value) const -{ - return QString::number(mkelvin_to_C((int)value)); -} - PartialGasPressureAxis::PartialGasPressureAxis(const DivePlotDataModel &model, Position position, int integralDigits, int fractionalDigits, color_index_t gridColor, double dpr, double labelScale, bool printMode, bool isGrayscale, ProfileScene &scene) : diff --git a/profile-widget/divecartesianaxis.h b/profile-widget/divecartesianaxis.h index 8b4fd3a03..55cd32ac8 100644 --- a/profile-widget/divecartesianaxis.h +++ b/profile-widget/divecartesianaxis.h @@ -32,10 +32,12 @@ public: enum class Position { Left, Right, Bottom }; + DiveCartesianAxis(Position position, int integralDigits, int fractionalDigits, color_index_t gridColor, double dpr, double labelScale, bool printMode, bool isGrayscale, ProfileScene &scene); ~DiveCartesianAxis(); void setBounds(double min, double max); + void setTransform(double a, double b = 0.0); void setTickInterval(double interval); void setOrientation(Orientation orientation); double minimum() const; @@ -56,6 +58,7 @@ signals: protected: Position position; + int fractionalDigits; QRectF rect; // Rectangle to fill with grid lines QPen gridPen; color_index_t gridColor; @@ -74,6 +77,16 @@ protected: bool changed; double dpr; double labelWidth, labelHeight; // maximum expected sizes of label width and height + + // To format the labels and choose the label positions, the + // axis has to be aware of the displayed values. Thankfully, + // the conversion between internal data (eg. mm) and displayed + // data (e.g. ft) can be represented by an affine map ax+b. + struct Transform { + double a, b; + double to(double x) const; + double from(double y) const; + } transform; }; class DepthAxis : public DiveCartesianAxis { @@ -81,7 +94,6 @@ class DepthAxis : public DiveCartesianAxis { public: using DiveCartesianAxis::DiveCartesianAxis; private: - QString textForValue(double value) const override; QColor colorForValue(double value) const override; }; @@ -99,8 +111,6 @@ class TemperatureAxis : public DiveCartesianAxis { Q_OBJECT public: using DiveCartesianAxis::DiveCartesianAxis; -private: - QString textForValue(double value) const override; }; class PartialGasPressureAxis : public DiveCartesianAxis { diff --git a/profile-widget/profilescene.cpp b/profile-widget/profilescene.cpp index 020930f17..c508e61a5 100644 --- a/profile-widget/profilescene.cpp +++ b/profile-widget/profilescene.cpp @@ -51,7 +51,7 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) : gasYAxis(new PartialGasPressureAxis(*dataModel, DiveCartesianAxis::Position::Right, 1, 2, TIME_GRID, dpr, 0.7, printMode, isGrayscale, *this)), temperatureAxis(new TemperatureAxis(DiveCartesianAxis::Position::Right, 3, 0, TIME_GRID, dpr, 1.0, printMode, isGrayscale, *this)), timeAxis(new TimeAxis(DiveCartesianAxis::Position::Bottom, 2, 2, TIME_GRID, dpr, 1.0, printMode, isGrayscale, *this)), - cylinderPressureAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Right, 2, 2, TIME_GRID, dpr, 1.0, printMode, isGrayscale, *this)), + cylinderPressureAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Right, 4, 0, TIME_GRID, dpr, 1.0, printMode, isGrayscale, *this)), heartBeatAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Left, 3, 0, HR_AXIS, dpr, 0.7, printMode, isGrayscale, *this)), percentageAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Right, 2, 0, TIME_GRID, dpr, 0.7, printMode, isGrayscale, *this)), diveProfileItem(createItem(*profileYAxis, DivePlotDataModel::DEPTH, 0, dpr)), @@ -115,6 +115,11 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) : gasYAxis->setZValue(timeAxis->zValue() + 1); tankItem->setZValue(100); + // These axes are not locale-dependent. Set their scale factor once here. + timeAxis->setTransform(60.0); + heartBeatAxis->setTransform(1.0); + gasYAxis->setTransform(1.0); // Non-metric countries likewise use bar (disguised as "percentage") for partial pressure. + for (int i = 0; i < 16; i++) { DiveCalculatedTissue *tissueItem = createItem(*profileYAxis, DivePlotDataModel::TISSUE_1 + i, i + 1, dpr); allTissues.append(tissueItem); @@ -199,6 +204,19 @@ void ProfileScene::resize(QSizeF size) setSceneRect(QRectF(QPointF(), size)); } +// Helper templates to determine slope and intersect of a linear function. +// The function arguments are supposed to be integral types. +template +static auto intercept(Func f) +{ + return f(0); +} +template +static auto slope(Func f) +{ + return f(1) - f(0); +} + // Helper structure for laying out secondary plots. struct VerticalAxisLayout { DiveCartesianAxis *axis; @@ -286,6 +304,16 @@ void ProfileScene::updateAxes(bool instant, bool diveHasHeartBeat) // The cylinders are displayed in the 24-80% region of the profile cylinderPressureAxis->animateChangeLine(QRectF(leftBorder, topBorder + 0.24 * height, width, 0.56 * height), animSpeed); + + // Set scale factors depending on locale. + // The conversion calls, such as mm_to_feet(), will be optimized away. + profileYAxis->setTransform(prefs.units.length == units::METERS ? 0.001 : slope(mm_to_feet)); + cylinderPressureAxis->setTransform(prefs.units.pressure == units::BAR ? 0.001 : slope(mbar_to_PSI)); + // Temperature is special: this is not a linear transformation, but requires a shift of origin. + if (prefs.units.temperature == units::CELSIUS) + temperatureAxis->setTransform(slope(mkelvin_to_C), intercept(mkelvin_to_C)); + else + temperatureAxis->setTransform(slope(mkelvin_to_F), intercept(mkelvin_to_F)); } bool ProfileScene::isPointOutOfBoundaries(const QPointF &point) const