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 <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2021-08-29 22:13:26 +02:00 committed by Dirk Hohndel
parent 24cf6709e3
commit 505e4e47eb
10 changed files with 177 additions and 89 deletions

View file

@ -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 \

View file

@ -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

View file

@ -359,6 +359,12 @@ double DiveCartesianAxis::minimum() const
return min;
}
std::pair<double, double> 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();

View file

@ -41,6 +41,7 @@ public:
void setFontLabelScale(qreal scale);
double minimum() const;
double maximum() const;
std::pair<double, double> 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

View file

@ -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 <array>
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<int, num_tissues> calcLinesPerTissue(int size)
{
std::array<int, num_tissues> 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<int, num_tissues> 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);
}

View file

@ -0,0 +1,23 @@
#ifndef DIVEPERCENTAGEITEM_H
#define DIVEPERCENTAGEITEM_H
#include <QGraphicsPixmapItem>
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

View file

@ -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)

View file

@ -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<QColor> 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

View file

@ -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<DiveCalculatedCeiling>(*profileYAxis, DivePlotDataModel::CEILING, 1, dpr)),
decoModelParameters(new DiveTextItem(dpr, 1.0, Qt::AlignHCenter | Qt::AlignTop, nullptr)),
heartBeatItem(createItem<DiveHeartrateItem>(*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<DiveCalculatedTissue>(*profileYAxis, DivePlotDataModel::TISSUE_1 + i, i + 1, dpr);
allTissues.append(tissueItem);
DivePercentageItem *percentageItem = createItem<DivePercentageItem>(*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.

View file

@ -96,7 +96,7 @@ private:
DiveTextItem *decoModelParameters;
QList<DiveCalculatedTissue *> allTissues;
DiveHeartrateItem *heartBeatItem;
QList<DivePercentageItem *> allPercentages;
DivePercentageItem *percentageItem;
TankItem *tankItem;
};