From 505e4e47ebed2f9836318ebb4243497de3b7fddb Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sun, 29 Aug 2021 22:13:26 +0200 Subject: [PATCH] profile: reimplement DivePercentageItem The tissue percentages were realized as 16 independent polygons. That didn't work at all with the new absolute scaling. Reimplement the item and blast it onto a pixmap. Not only is this artifact-free, it also should (hopefully) be quite a bit more efficient than painting numerous lines. In contrast to the old code, this does access the plot_info structure directly instead of using the model. Not so much for performance reason, but rather to make things more robust: We have a strongly typed language. Why would we shoehorn data through the weakly typed QVariant and mess with wierd index-arithmetics. Makes no sense to me. Qt-model have to be used for interfacing with Qt. They are terrible for intra-application data transfer. Signed-off-by: Berthold Stoeger --- Subsurface-mobile.pro | 2 + profile-widget/CMakeLists.txt | 2 + profile-widget/divecartesianaxis.cpp | 6 ++ profile-widget/divecartesianaxis.h | 3 +- profile-widget/divepercentageitem.cpp | 132 ++++++++++++++++++++++++++ profile-widget/divepercentageitem.h | 23 +++++ profile-widget/diveprofileitem.cpp | 68 ------------- profile-widget/diveprofileitem.h | 15 --- profile-widget/profilescene.cpp | 13 ++- profile-widget/profilescene.h | 2 +- 10 files changed, 177 insertions(+), 89 deletions(-) create mode 100644 profile-widget/divepercentageitem.cpp create mode 100644 profile-widget/divepercentageitem.h diff --git a/Subsurface-mobile.pro b/Subsurface-mobile.pro index 3de851336..4db369411 100644 --- a/Subsurface-mobile.pro +++ b/Subsurface-mobile.pro @@ -168,6 +168,7 @@ SOURCES += subsurface-mobile-main.cpp \ profile-widget/qmlprofile.cpp \ profile-widget/divecartesianaxis.cpp \ profile-widget/diveeventitem.cpp \ + profile-widget/divepercentageitem.cpp \ profile-widget/diveprofileitem.cpp \ profile-widget/profilescene.cpp \ profile-widget/animationfunctions.cpp \ @@ -321,6 +322,7 @@ HEADERS += \ qt-models/filterconstraintmodel.h \ qt-models/filterpresetmodel.h \ profile-widget/qmlprofile.h \ + profile-widget/divepercentageitem.h \ profile-widget/diveprofileitem.h \ profile-widget/profilescene.h \ profile-widget/diveeventitem.h \ diff --git a/profile-widget/CMakeLists.txt b/profile-widget/CMakeLists.txt index e7a21b12b..eb89c3eb3 100644 --- a/profile-widget/CMakeLists.txt +++ b/profile-widget/CMakeLists.txt @@ -10,6 +10,8 @@ set(SUBSURFACE_PROFILE_LIB_SRCS divelineitem.h divepixmapitem.cpp divepixmapitem.h + divepercentageitem.cpp + divepercentageitem.h diveprofileitem.cpp diveprofileitem.h diverectitem.cpp diff --git a/profile-widget/divecartesianaxis.cpp b/profile-widget/divecartesianaxis.cpp index 7b0d650be..6a1a13692 100644 --- a/profile-widget/divecartesianaxis.cpp +++ b/profile-widget/divecartesianaxis.cpp @@ -359,6 +359,12 @@ double DiveCartesianAxis::minimum() const return min; } +std::pair DiveCartesianAxis::screenMinMax() const +{ + return position == Position::Bottom ? std::make_pair(rect.left(), rect.right()) + : std::make_pair(rect.top(), rect.bottom()); +} + void DiveCartesianAxis::setColor(const QColor &color) { QPen defaultPen = gridPen(); diff --git a/profile-widget/divecartesianaxis.h b/profile-widget/divecartesianaxis.h index 122bec3cd..9880bb4b5 100644 --- a/profile-widget/divecartesianaxis.h +++ b/profile-widget/divecartesianaxis.h @@ -41,6 +41,7 @@ public: void setFontLabelScale(qreal scale); double minimum() const; double maximum() const; + std::pair screenMinMax() const; qreal valueAt(const QPointF &p) const; qreal posAtValue(qreal value) const; void setColor(const QColor &color); @@ -48,7 +49,7 @@ public: void animateChangeLine(const QRectF &rect, int animSpeed); void setTextVisible(bool arg1); void setLinesVisible(bool arg1); - void setLine(const QLineF& line); + void setLine(const QLineF &line); virtual void updateTicks(int animSpeed); double width() const; // only for vertical axes double height() const; // only for horizontal axes diff --git a/profile-widget/divepercentageitem.cpp b/profile-widget/divepercentageitem.cpp new file mode 100644 index 000000000..fb40553e5 --- /dev/null +++ b/profile-widget/divepercentageitem.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "divepercentageitem.h" +#include "divecartesianaxis.h" +#include "core/dive.h" +#include "core/profile.h" + +#include + +DivePercentageItem::DivePercentageItem(const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, double dpr) : + hAxis(hAxis), vAxis(vAxis), dpr(dpr) +{ +} + +static constexpr int num_tissues = 16; + +// Calculate the number of scanlines for every drawn tissue. +static std::array calcLinesPerTissue(int size) +{ + std::array res; + + // A Bresenham-inspired algorithm without the weird half steps at the beginning and the end. + if (size <= 0) { + std::fill(res.begin(), res.end(), 0); + } else if (size >= num_tissues) { + int step = size / num_tissues; + int err_inc = size % num_tissues; + int err = 0; + for (int i = 0; i < num_tissues; ++i) { + res[i] = step; + err += err_inc; + if (err >= num_tissues) { + err -= num_tissues; + ++res[i]; + } + } + } else { // size < num_tissues + int step = num_tissues / size; + int err_inc = num_tissues % size; + int err = 0; + int act = 0; + std::fill(res.begin(), res.end(), 0); + for (int i = 0; i < size; ++i) { + res[act] = 1; + act += step; + err += err_inc; + if (err >= size) { + err -= size; + ++act; + } + } + } + return res; +} + +static QRgb colorScale(double value, int inert) +{ + QColor color; + double scaledValue = value / (AMB_PERCENTAGE * inert) * 1000.0; + if (scaledValue < 0.8) // grade from cyan to blue to purple + color.setHsvF(0.5 + 0.25 * scaledValue / 0.8, 1.0, 1.0); + else if (scaledValue < 1.0) // grade from magenta to black + color.setHsvF(0.75, 1.0, (1.0 - scaledValue) / 0.2); + else if (value < AMB_PERCENTAGE) // grade from black to bright green + color.setHsvF(0.333, 1.0, (value - AMB_PERCENTAGE * inert / 1000.0) / (AMB_PERCENTAGE - AMB_PERCENTAGE * inert / 1000.0)); + else if (value < 65) // grade from bright green (0% M) to yellow-green (30% M) + color.setHsvF(0.333 - 0.133 * (value - AMB_PERCENTAGE) / (65.0 - AMB_PERCENTAGE), 1.0, 1.0); + else if (value < 85) // grade from yellow-green (30% M) to orange (70% M) + color.setHsvF(0.2 - 0.1 * (value - 65.0) / 20.0, 1.0, 1.0); + else if (value < 100) // grade from orange (70% M) to red (100% M) + color.setHsvF(0.1 * (100.0 - value) / 15.0, 1.0, 1.0); + else if (value < 120) // M value exceeded - grade from red to white + color.setHsvF(0.0, 1 - (value - 100.0) / 20.0, 1.0); + else // white + color.setHsvF(0.0, 0.0, 1.0); + return color.rgba(); +} + +void DivePercentageItem::replot(const dive *d, const struct divecomputer *dc, const plot_info &pi) +{ + auto [minX, maxX] = hAxis.screenMinMax(); + auto [minY, maxY] = vAxis.screenMinMax(); + int width = lrint(maxX) - lrint(minX); + int height = lrint(maxY) - lrint(minY); + if (width <= 0 || height <= 0) { + setPixmap(QPixmap()); + return; + } + + std::array linesPerTissue = calcLinesPerTissue(height); + + QImage img(width, height, QImage::QImage::Format_ARGB32); + + int line = 0; + for (int tissue = 0; tissue < num_tissues; ++tissue) { + if (linesPerTissue[tissue] <= 0) + continue; + int x = 0; + QRgb *scanline = (QRgb *)img.scanLine(line); + QRgb color = 0; + const struct event *ev = NULL; + for (int i = 0; i < pi.nr; i++) { + const plot_data &item = pi.entry[i]; + int sec = item.sec; + int nextX = lrint(hAxis.posAtValue(sec)) - lrint(minX); + if (nextX == x) + continue; + + double value = item.percentages[tissue]; + struct gasmix gasmix = get_gasmix(d, dc, sec, &ev, gasmix); + int inert = get_n2(gasmix) + get_he(gasmix); + color = colorScale(value, inert); + if (nextX >= width) + nextX = width - 1; + for (; x <= nextX; ++x) + scanline[x] = color; + if (nextX >= width - 1) + break; + } + for (; x < width; ++x) + scanline[x] = color; + ++line; + + // Clone line if needed + for (int i = 0; i < linesPerTissue[tissue] - 1; ++i) { + QRgb *scanline2 = (QRgb *)img.scanLine(line); + std::copy(scanline, scanline + width, scanline2); + ++line; + } + } + setPixmap(QPixmap::fromImage(img)); + setPos(minX, minY); +} diff --git a/profile-widget/divepercentageitem.h b/profile-widget/divepercentageitem.h new file mode 100644 index 000000000..72bfaceca --- /dev/null +++ b/profile-widget/divepercentageitem.h @@ -0,0 +1,23 @@ +#ifndef DIVEPERCENTAGEITEM_H +#define DIVEPERCENTAGEITEM_H + +#include + +struct dive; +struct divecomputer; +struct plot_info; +class DivePlotDataModel; +class DiveCartesianAxis; + +class DivePercentageItem : public QGraphicsPixmapItem { +public: + DivePercentageItem(const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, double dpr); + void replot(const dive *d, const divecomputer *dc, const plot_info &pi); +private: + const DiveCartesianAxis &hAxis; + const DiveCartesianAxis &vAxis; + int hDataColumn; + double dpr; +}; + +#endif diff --git a/profile-widget/diveprofileitem.cpp b/profile-widget/diveprofileitem.cpp index f4069ef63..906effcc5 100644 --- a/profile-widget/diveprofileitem.cpp +++ b/profile-widget/diveprofileitem.cpp @@ -233,74 +233,6 @@ void DiveHeartrateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem* painter->restore(); } -DivePercentageItem::DivePercentageItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, - const DiveCartesianAxis &vAxis, int vColumn, int i, double dpr) : - AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr), - tissueIndex(i) -{ -} - -void DivePercentageItem::replot(const dive *d, bool) -{ - // Ignore empty values. a heart rate of 0 would be a bad sign. - QPolygonF poly; - colors.clear(); - for (int i = 0, modelDataCount = dataModel.rowCount(); i < modelDataCount; i++) { - int sec = dataModel.index(i, hDataColumn).data().toInt(); - QPointF point(hAxis.posAtValue(sec), vAxis.posAtValue(64 - 4 * tissueIndex)); - poly.append(point); - - double value = dataModel.index(i, vDataColumn).data().toDouble(); - struct gasmix gasmix = gasmix_air; - const struct event *ev = NULL; - gasmix = get_gasmix(d, get_dive_dc_const(d, dc_number), sec, &ev, gasmix); - int inert = get_n2(gasmix) + get_he(gasmix); - colors.push_back(ColorScale(value, inert)); - } - setPolygon(poly); -} - -QColor DivePercentageItem::ColorScale(double value, int inert) -{ - QColor color; - double scaledValue = value / (AMB_PERCENTAGE * inert) * 1000.0; - if (scaledValue < 0.8) // grade from cyan to blue to purple - color.setHsvF(0.5 + 0.25 * scaledValue / 0.8, 1.0, 1.0); - else if (scaledValue < 1.0) // grade from magenta to black - color.setHsvF(0.75, 1.0, (1.0 - scaledValue) / 0.2); - else if (value < AMB_PERCENTAGE) // grade from black to bright green - color.setHsvF(0.333, 1.0, (value - AMB_PERCENTAGE * inert / 1000.0) / (AMB_PERCENTAGE - AMB_PERCENTAGE * inert / 1000.0)); - else if (value < 65) // grade from bright green (0% M) to yellow-green (30% M) - color.setHsvF(0.333 - 0.133 * (value - AMB_PERCENTAGE) / (65.0 - AMB_PERCENTAGE), 1.0, 1.0); - else if (value < 85) // grade from yellow-green (30% M) to orange (70% M) - color.setHsvF(0.2 - 0.1 * (value - 65.0) / 20.0, 1.0, 1.0); - else if (value < 100) // grade from orange (70% M) to red (100% M) - color.setHsvF(0.1 * (100.0 - value) / 15.0, 1.0, 1.0); - else if (value < 120) // M value exceeded - grade from red to white - color.setHsvF(0.0, 1 - (value - 100.0) / 20.0, 1.0); - else // white - color.setHsvF(0.0, 0.0, 1.0); - return color; - -} - -void DivePercentageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) -{ - if (polygon().isEmpty()) - return; - painter->save(); - QPen mypen; - mypen.setCapStyle(Qt::FlatCap); - mypen.setCosmetic(false); - QPolygonF poly = polygon(); - for (int i = 1; i < poly.count(); i++) { - mypen.setBrush(QBrush(colors[i])); - painter->setPen(mypen); - painter->drawLine(poly[i - 1], poly[i]); - } - painter->restore(); -} - DiveTemperatureItem::DiveTemperatureItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr) diff --git a/profile-widget/diveprofileitem.h b/profile-widget/diveprofileitem.h index f5bcd3fe2..d0d3215be 100644 --- a/profile-widget/diveprofileitem.h +++ b/profile-widget/diveprofileitem.h @@ -97,21 +97,6 @@ private: QString visibilityKey; }; -class DivePercentageItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DivePercentageItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, int i, double dpr); - void replot(const dive *d, bool in_planner) override; - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - -private: - std::vector colors; // Must have same number of elements as the polygon - QString visibilityKey; - int tissueIndex; - QColor ColorScale(double value, int inert); - -}; - class DiveGasPressureItem : public AbstractProfilePolygonItem { Q_OBJECT diff --git a/profile-widget/profilescene.cpp b/profile-widget/profilescene.cpp index 6f6d9de9e..d8fc9c9d1 100644 --- a/profile-widget/profilescene.cpp +++ b/profile-widget/profilescene.cpp @@ -2,6 +2,7 @@ #include "profilescene.h" #include "diveeventitem.h" #include "divecartesianaxis.h" +#include "divepercentageitem.h" #include "diveprofileitem.h" #include "divetextitem.h" #include "tankitem.h" @@ -69,6 +70,7 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) : diveCeiling(createItem(*profileYAxis, DivePlotDataModel::CEILING, 1, dpr)), decoModelParameters(new DiveTextItem(dpr, 1.0, Qt::AlignHCenter | Qt::AlignTop, nullptr)), heartBeatItem(createItem(*heartBeatAxis, DivePlotDataModel::HEARTBEAT, 1, dpr)), + percentageItem(new DivePercentageItem(*timeAxis, *percentageAxis, dpr)), tankItem(new TankItem(*timeAxis, dpr)) { init_plot_info(&plotInfo); @@ -119,10 +121,10 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) : for (int i = 0; i < 16; i++) { DiveCalculatedTissue *tissueItem = createItem(*profileYAxis, DivePlotDataModel::TISSUE_1 + i, i + 1, dpr); allTissues.append(tissueItem); - DivePercentageItem *percentageItem = createItem(*percentageAxis, DivePlotDataModel::PERCENTAGE_1 + i, i + 1, i, dpr); - allPercentages.append(percentageItem); } + percentageItem->setZValue(1.0); + // Add items to scene addItem(diveComputerText); addItem(tankItem); @@ -134,6 +136,7 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) : addItem(cylinderPressureAxis); addItem(percentageAxis); addItem(heartBeatAxis); + addItem(percentageItem); for (AbstractProfilePolygonItem *item: profileItems) addItem(item); @@ -189,8 +192,7 @@ void ProfileScene::updateVisibility() #ifndef SUBSURFACE_MOBILE for (DiveCalculatedTissue *tissue: allTissues) tissue->setVisible(prefs.calcalltissues && prefs.calcceiling); - for (DivePercentageItem *percentage: allPercentages) - percentage->setVisible(prefs.percentagegraph); + percentageItem->setVisible(prefs.percentagegraph); #endif meanDepthItem->setVisible(prefs.show_average_depth); reportedCeiling->setVisible(prefs.dcceiling); @@ -468,6 +470,9 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM for (AbstractProfilePolygonItem *item: profileItems) item->replot(d, inPlanner); + if (prefs.percentagegraph) + percentageItem->replot(d, currentdc, dataModel->data()); + // The event items are a bit special since we don't know how many events are going to // exist on a dive, so I cant create cache items for that. that's why they are here // while all other items are up there on the constructor. diff --git a/profile-widget/profilescene.h b/profile-widget/profilescene.h index 7424d66f8..cb8bc8ce4 100644 --- a/profile-widget/profilescene.h +++ b/profile-widget/profilescene.h @@ -96,7 +96,7 @@ private: DiveTextItem *decoModelParameters; QList allTissues; DiveHeartrateItem *heartBeatItem; - QList allPercentages; + DivePercentageItem *percentageItem; TankItem *tankItem; };