2021-06-05 16:42:56 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
#include "profilescene.h"
|
2021-08-03 07:51:37 +00:00
|
|
|
#include "diveeventitem.h"
|
2021-06-28 18:29:46 +00:00
|
|
|
#include "divecartesianaxis.h"
|
2021-08-03 07:51:37 +00:00
|
|
|
#include "diveprofileitem.h"
|
|
|
|
#include "divetextitem.h"
|
|
|
|
#include "tankitem.h"
|
|
|
|
#include "core/device.h"
|
|
|
|
#include "core/event.h"
|
2021-06-28 18:29:46 +00:00
|
|
|
#include "core/pref.h"
|
2021-08-03 07:51:37 +00:00
|
|
|
#include "core/profile.h"
|
|
|
|
#include "core/qthelper.h" // for decoMode()
|
|
|
|
#include "core/subsurface-string.h"
|
|
|
|
#include "core/settings/qPrefDisplay.h"
|
2021-06-28 18:29:46 +00:00
|
|
|
#include "qt-models/diveplotdatamodel.h"
|
2021-08-03 07:51:37 +00:00
|
|
|
#include "qt-models/diveplannermodel.h"
|
2021-06-05 16:42:56 +00:00
|
|
|
|
2021-06-28 18:29:46 +00:00
|
|
|
const static struct ProfileItemPos {
|
|
|
|
struct Pos {
|
|
|
|
QPointF on;
|
|
|
|
QPointF off;
|
|
|
|
};
|
|
|
|
struct Axis : public Pos {
|
|
|
|
QLineF shrinked;
|
|
|
|
QLineF expanded;
|
|
|
|
QLineF intermediate;
|
|
|
|
};
|
2021-08-03 07:51:37 +00:00
|
|
|
Pos dcLabel;
|
|
|
|
Pos tankBar;
|
2021-06-28 18:29:46 +00:00
|
|
|
Axis depth;
|
|
|
|
Axis partialPressure;
|
|
|
|
Axis partialPressureTissue;
|
|
|
|
Axis partialPressureWithTankBar;
|
|
|
|
Axis percentage;
|
|
|
|
Axis percentageWithTankBar;
|
|
|
|
Axis time;
|
|
|
|
Axis cylinder;
|
|
|
|
Axis temperature;
|
|
|
|
Axis temperatureAll;
|
|
|
|
Axis heartBeat;
|
|
|
|
Axis heartBeatWithTankBar;
|
|
|
|
ProfileItemPos();
|
|
|
|
} itemPos;
|
|
|
|
|
2021-08-03 07:51:37 +00:00
|
|
|
template<typename T, class... Args>
|
|
|
|
T *ProfileScene::createItem(const DiveCartesianAxis &vAxis, int vColumn, int z, Args&&... args)
|
|
|
|
{
|
|
|
|
T *res = new T(*dataModel, *timeAxis, DivePlotDataModel::TIME, vAxis, vColumn,
|
|
|
|
std::forward<Args>(args)...);
|
|
|
|
res->setZValue(static_cast<double>(z));
|
|
|
|
profileItems.push_back(res);
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
PartialPressureGasItem *ProfileScene::createPPGas(int column, color_index_t color, color_index_t colorAlert,
|
|
|
|
const double *thresholdSettingsMin, const double *thresholdSettingsMax)
|
|
|
|
{
|
2021-08-09 14:48:08 +00:00
|
|
|
PartialPressureGasItem *item = createItem<PartialPressureGasItem>(*gasYAxis, column, 99, dpr);
|
2021-08-03 07:51:37 +00:00
|
|
|
item->setThresholdSettingsKey(thresholdSettingsMin, thresholdSettingsMax);
|
|
|
|
item->setColors(getColor(color, isGrayscale), getColor(colorAlert, isGrayscale));
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
2021-08-09 14:48:08 +00:00
|
|
|
ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) :
|
2021-08-03 07:51:37 +00:00
|
|
|
d(nullptr),
|
|
|
|
dc(-1),
|
2021-08-09 14:48:08 +00:00
|
|
|
dpr(dpr),
|
2021-08-03 13:39:25 +00:00
|
|
|
printMode(printMode),
|
|
|
|
isGrayscale(isGrayscale),
|
2021-08-03 07:51:37 +00:00
|
|
|
maxtime(-1),
|
|
|
|
maxdepth(-1),
|
2021-06-28 18:29:46 +00:00
|
|
|
dataModel(new DivePlotDataModel(this)),
|
2021-08-28 18:12:36 +00:00
|
|
|
profileYAxis(new DepthAxis(DiveCartesianAxis::Position::Left, TIME_GRID, dpr, printMode, *this)),
|
|
|
|
gasYAxis(new PartialGasPressureAxis(*dataModel, DiveCartesianAxis::Position::Right, TIME_GRID, dpr, printMode, *this)),
|
|
|
|
temperatureAxis(new TemperatureAxis(DiveCartesianAxis::Position::Right, TIME_GRID, dpr, printMode, *this)),
|
|
|
|
timeAxis(new TimeAxis(DiveCartesianAxis::Position::Bottom, TIME_GRID, dpr, printMode, *this)),
|
|
|
|
cylinderPressureAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Right, TIME_GRID, dpr, printMode, *this)),
|
|
|
|
heartBeatAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Left, HR_AXIS, dpr, printMode, *this)),
|
|
|
|
percentageAxis(new DiveCartesianAxis(DiveCartesianAxis::Position::Right, TIME_GRID, dpr, printMode, *this)),
|
2021-08-09 14:48:08 +00:00
|
|
|
diveProfileItem(createItem<DiveProfileItem>(*profileYAxis, DivePlotDataModel::DEPTH, 0, dpr)),
|
|
|
|
temperatureItem(createItem<DiveTemperatureItem>(*temperatureAxis, DivePlotDataModel::TEMPERATURE, 1, dpr)),
|
|
|
|
meanDepthItem(createItem<DiveMeanDepthItem>(*profileYAxis, DivePlotDataModel::INSTANT_MEANDEPTH, 1, dpr)),
|
|
|
|
gasPressureItem(createItem<DiveGasPressureItem>(*cylinderPressureAxis, DivePlotDataModel::TEMPERATURE, 1, dpr)),
|
2021-08-12 20:57:57 +00:00
|
|
|
diveComputerText(new DiveTextItem(dpr, 1.0, Qt::AlignRight | Qt::AlignTop, nullptr)),
|
2021-08-09 14:48:08 +00:00
|
|
|
reportedCeiling(createItem<DiveReportedCeiling>(*profileYAxis, DivePlotDataModel::CEILING, 1, dpr)),
|
2021-08-03 07:51:37 +00:00
|
|
|
pn2GasItem(createPPGas(DivePlotDataModel::PN2, PN2, PN2_ALERT, NULL, &prefs.pp_graphs.pn2_threshold)),
|
|
|
|
pheGasItem(createPPGas(DivePlotDataModel::PHE, PHE, PHE_ALERT, NULL, &prefs.pp_graphs.phe_threshold)),
|
|
|
|
po2GasItem(createPPGas(DivePlotDataModel::PO2, PO2, PO2_ALERT, &prefs.pp_graphs.po2_threshold_min, &prefs.pp_graphs.po2_threshold_max)),
|
|
|
|
o2SetpointGasItem(createPPGas(DivePlotDataModel::O2SETPOINT, O2SETPOINT, PO2_ALERT, &prefs.pp_graphs.po2_threshold_min, &prefs.pp_graphs.po2_threshold_max)),
|
|
|
|
ccrsensor1GasItem(createPPGas(DivePlotDataModel::CCRSENSOR1, CCRSENSOR1, PO2_ALERT, &prefs.pp_graphs.po2_threshold_min, &prefs.pp_graphs.po2_threshold_max)),
|
|
|
|
ccrsensor2GasItem(createPPGas(DivePlotDataModel::CCRSENSOR2, CCRSENSOR2, PO2_ALERT, &prefs.pp_graphs.po2_threshold_min, &prefs.pp_graphs.po2_threshold_max)),
|
|
|
|
ccrsensor3GasItem(createPPGas(DivePlotDataModel::CCRSENSOR3, CCRSENSOR3, PO2_ALERT, &prefs.pp_graphs.po2_threshold_min, &prefs.pp_graphs.po2_threshold_max)),
|
|
|
|
ocpo2GasItem(createPPGas(DivePlotDataModel::SCR_OC_PO2, SCR_OCPO2, PO2_ALERT, &prefs.pp_graphs.po2_threshold_min, &prefs.pp_graphs.po2_threshold_max)),
|
2021-08-09 14:48:08 +00:00
|
|
|
diveCeiling(createItem<DiveCalculatedCeiling>(*profileYAxis, DivePlotDataModel::CEILING, 1, dpr)),
|
2021-08-12 20:57:57 +00:00
|
|
|
decoModelParameters(new DiveTextItem(dpr, 1.0, Qt::AlignHCenter | Qt::AlignBottom, nullptr)),
|
2021-08-09 14:48:08 +00:00
|
|
|
heartBeatItem(createItem<DiveHeartrateItem>(*heartBeatAxis, DivePlotDataModel::HEARTBEAT, 1, dpr)),
|
|
|
|
tankItem(new TankItem(*timeAxis, dpr))
|
2021-06-05 16:42:56 +00:00
|
|
|
{
|
2021-08-03 07:51:37 +00:00
|
|
|
init_plot_info(&plotInfo);
|
|
|
|
|
2021-06-05 16:49:00 +00:00
|
|
|
setSceneRect(0, 0, 100, 100);
|
|
|
|
setItemIndexMethod(QGraphicsScene::NoIndex);
|
2021-06-28 18:29:46 +00:00
|
|
|
|
|
|
|
// Initialize axes. Perhaps part of this should be moved down to the axes code?
|
|
|
|
profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom);
|
|
|
|
profileYAxis->setMinimum(0);
|
|
|
|
profileYAxis->setTickInterval(M_OR_FT(10, 30));
|
|
|
|
profileYAxis->setTickSize(0.5);
|
|
|
|
profileYAxis->setLineSize(96);
|
|
|
|
|
|
|
|
timeAxis->setLineSize(92);
|
|
|
|
timeAxis->setTickSize(-0.5);
|
|
|
|
|
|
|
|
gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop);
|
|
|
|
gasYAxis->setTickInterval(1);
|
|
|
|
gasYAxis->setTickSize(1);
|
|
|
|
gasYAxis->setMinimum(0);
|
|
|
|
gasYAxis->setFontLabelScale(0.7);
|
|
|
|
gasYAxis->setLineSize(96);
|
|
|
|
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop);
|
|
|
|
heartBeatAxis->setTickSize(0.2);
|
|
|
|
heartBeatAxis->setTickInterval(10);
|
|
|
|
heartBeatAxis->setFontLabelScale(0.7);
|
|
|
|
heartBeatAxis->setLineSize(96);
|
|
|
|
|
|
|
|
percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop);
|
|
|
|
percentageAxis->setTickSize(0.2);
|
|
|
|
percentageAxis->setTickInterval(10);
|
|
|
|
percentageAxis->setFontLabelScale(0.7);
|
|
|
|
percentageAxis->setLineSize(96);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop);
|
|
|
|
temperatureAxis->setTickSize(2);
|
|
|
|
temperatureAxis->setTickInterval(300);
|
|
|
|
|
|
|
|
cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop);
|
|
|
|
cylinderPressureAxis->setTickSize(2);
|
|
|
|
cylinderPressureAxis->setTickInterval(30000);
|
|
|
|
|
|
|
|
heartBeatAxis->setTextVisible(true);
|
|
|
|
heartBeatAxis->setLinesVisible(true);
|
|
|
|
percentageAxis->setTextVisible(true);
|
|
|
|
percentageAxis->setLinesVisible(true);
|
|
|
|
|
|
|
|
temperatureAxis->setTextVisible(false);
|
|
|
|
temperatureAxis->setLinesVisible(false);
|
|
|
|
cylinderPressureAxis->setTextVisible(false);
|
|
|
|
cylinderPressureAxis->setLinesVisible(false);
|
|
|
|
timeAxis->setLinesVisible(true);
|
|
|
|
profileYAxis->setLinesVisible(true);
|
|
|
|
gasYAxis->setZValue(timeAxis->zValue() + 1);
|
2021-08-03 07:51:37 +00:00
|
|
|
tankItem->setZValue(100);
|
|
|
|
|
|
|
|
// show the deco model parameters at the top in the center
|
|
|
|
decoModelParameters->setY(0);
|
|
|
|
decoModelParameters->setX(50);
|
|
|
|
|
|
|
|
diveComputerText->setPos(itemPos.dcLabel.on);
|
|
|
|
|
|
|
|
tankItem->setPos(itemPos.tankBar.on);
|
|
|
|
|
|
|
|
for (int i = 0; i < 16; i++) {
|
2021-08-09 14:48:08 +00:00
|
|
|
DiveCalculatedTissue *tissueItem = createItem<DiveCalculatedTissue>(*profileYAxis, DivePlotDataModel::TISSUE_1 + i, i + 1, dpr);
|
2021-08-03 07:51:37 +00:00
|
|
|
allTissues.append(tissueItem);
|
2021-08-09 14:48:08 +00:00
|
|
|
DivePercentageItem *percentageItem = createItem<DivePercentageItem>(*percentageAxis, DivePlotDataModel::PERCENTAGE_1 + i, i + 1, i, dpr);
|
2021-08-03 07:51:37 +00:00
|
|
|
allPercentages.append(percentageItem);
|
|
|
|
}
|
2021-06-28 18:29:46 +00:00
|
|
|
|
|
|
|
// Add items to scene
|
2021-08-03 07:51:37 +00:00
|
|
|
addItem(diveComputerText);
|
|
|
|
addItem(tankItem);
|
|
|
|
addItem(decoModelParameters);
|
2021-06-28 18:29:46 +00:00
|
|
|
addItem(profileYAxis);
|
|
|
|
addItem(gasYAxis);
|
|
|
|
addItem(temperatureAxis);
|
|
|
|
addItem(timeAxis);
|
|
|
|
addItem(cylinderPressureAxis);
|
|
|
|
addItem(percentageAxis);
|
|
|
|
addItem(heartBeatAxis);
|
2021-08-03 07:51:37 +00:00
|
|
|
|
|
|
|
for (AbstractProfilePolygonItem *item: profileItems)
|
|
|
|
addItem(item);
|
2021-08-04 05:27:41 +00:00
|
|
|
|
|
|
|
updateAxes(true);
|
2021-06-05 16:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ProfileScene::~ProfileScene()
|
|
|
|
{
|
2021-08-03 07:51:37 +00:00
|
|
|
free_plot_info_data(&plotInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProfileScene::clear()
|
|
|
|
{
|
|
|
|
dataModel->clear();
|
|
|
|
|
|
|
|
for (AbstractProfilePolygonItem *item: profileItems)
|
|
|
|
item->clear();
|
|
|
|
|
|
|
|
// the events will have connected slots which can fire after
|
|
|
|
// the dive and its data have been deleted - so explictly delete
|
|
|
|
// the DiveEventItems
|
|
|
|
qDeleteAll(eventItems);
|
|
|
|
eventItems.clear();
|
2021-06-05 16:42:56 +00:00
|
|
|
}
|
2021-06-28 18:29:46 +00:00
|
|
|
|
|
|
|
static bool ppGraphsEnabled()
|
|
|
|
{
|
|
|
|
return prefs.pp_graphs.po2 || prefs.pp_graphs.pn2 || prefs.pp_graphs.phe;
|
|
|
|
}
|
|
|
|
|
2021-08-03 07:51:37 +00:00
|
|
|
// Update visibility of non-interactive chart features according to preferences
|
|
|
|
void ProfileScene::updateVisibility()
|
|
|
|
{
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
pn2GasItem->setVisible(prefs.pp_graphs.pn2);
|
|
|
|
po2GasItem->setVisible(prefs.pp_graphs.po2);
|
|
|
|
pheGasItem->setVisible(prefs.pp_graphs.phe);
|
|
|
|
|
|
|
|
const struct divecomputer *currentdc = d ? get_dive_dc_const(d, dc) : nullptr;
|
|
|
|
bool setpointflag = currentdc && currentdc->divemode == CCR && prefs.pp_graphs.po2;
|
|
|
|
bool sensorflag = setpointflag && prefs.show_ccr_sensors;
|
|
|
|
o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint);
|
|
|
|
ccrsensor1GasItem->setVisible(sensorflag);
|
|
|
|
ccrsensor2GasItem->setVisible(currentdc && sensorflag && currentdc->no_o2sensors > 1);
|
|
|
|
ccrsensor3GasItem->setVisible(currentdc && sensorflag && currentdc->no_o2sensors > 2);
|
|
|
|
ocpo2GasItem->setVisible(currentdc && currentdc->divemode == PSCR && prefs.show_scr_ocpo2);
|
|
|
|
|
|
|
|
heartBeatItem->setVisible(prefs.hrgraph);
|
|
|
|
#endif
|
|
|
|
diveCeiling->setVisible(prefs.calcceiling);
|
|
|
|
decoModelParameters->setVisible(prefs.calcceiling);
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
for (DiveCalculatedTissue *tissue: allTissues)
|
|
|
|
tissue->setVisible(prefs.calcalltissues && prefs.calcceiling);
|
|
|
|
for (DivePercentageItem *percentage: allPercentages)
|
|
|
|
percentage->setVisible(prefs.percentagegraph);
|
|
|
|
#endif
|
|
|
|
meanDepthItem->setVisible(prefs.show_average_depth);
|
|
|
|
reportedCeiling->setVisible(prefs.dcceiling);
|
|
|
|
tankItem->setVisible(prefs.tankbar);
|
|
|
|
}
|
|
|
|
|
2021-08-03 08:32:08 +00:00
|
|
|
void ProfileScene::updateAxes(bool instant)
|
2021-06-28 18:29:46 +00:00
|
|
|
{
|
2021-08-03 08:32:08 +00:00
|
|
|
int animSpeed = instant || printMode ? 0 : qPrefDisplay::animation_speed();
|
|
|
|
|
2021-06-28 18:29:46 +00:00
|
|
|
profileYAxis->setPos(itemPos.depth.on);
|
|
|
|
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
2021-08-03 08:32:08 +00:00
|
|
|
gasYAxis->update(animSpeed); // Initialize ticks of partial pressure graph
|
2021-06-28 18:29:46 +00:00
|
|
|
if ((prefs.percentagegraph||prefs.hrgraph) && ppGraphsEnabled()) {
|
2021-08-03 08:32:08 +00:00
|
|
|
profileYAxis->animateChangeLine(itemPos.depth.shrinked, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
temperatureAxis->setPos(itemPos.temperatureAll.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
temperatureAxis->animateChangeLine(itemPos.temperature.shrinked, animSpeed);
|
|
|
|
cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
|
|
|
|
if (prefs.tankbar) {
|
|
|
|
percentageAxis->setPos(itemPos.percentageWithTankBar.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
} else {
|
|
|
|
percentageAxis->setPos(itemPos.percentage.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
percentageAxis->animateChangeLine(itemPos.percentage.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
heartBeatAxis->setPos(itemPos.heartBeat.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
}
|
|
|
|
gasYAxis->setPos(itemPos.partialPressureTissue.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
} else if (ppGraphsEnabled() || prefs.hrgraph || prefs.percentagegraph) {
|
2021-08-03 08:32:08 +00:00
|
|
|
profileYAxis->animateChangeLine(itemPos.depth.intermediate, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
temperatureAxis->setPos(itemPos.temperature.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
temperatureAxis->animateChangeLine(itemPos.temperature.intermediate, animSpeed);
|
|
|
|
cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
if (prefs.tankbar) {
|
|
|
|
percentageAxis->setPos(itemPos.percentageWithTankBar.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
gasYAxis->setPos(itemPos.partialPressureWithTankBar.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
gasYAxis->animateChangeLine(itemPos.partialPressureWithTankBar.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
} else {
|
|
|
|
gasYAxis->setPos(itemPos.partialPressure.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
gasYAxis->animateChangeLine(itemPos.partialPressure.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
percentageAxis->setPos(itemPos.percentage.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
percentageAxis->animateChangeLine(itemPos.percentage.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
heartBeatAxis->setPos(itemPos.heartBeat.on);
|
2021-08-03 08:32:08 +00:00
|
|
|
heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
#else
|
|
|
|
{
|
|
|
|
#endif
|
2021-08-03 08:32:08 +00:00
|
|
|
profileYAxis->animateChangeLine(itemPos.depth.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
if (prefs.tankbar) {
|
|
|
|
temperatureAxis->setPos(itemPos.temperatureAll.on);
|
|
|
|
} else {
|
|
|
|
temperatureAxis->setPos(itemPos.temperature.on);
|
|
|
|
}
|
2021-08-03 08:32:08 +00:00
|
|
|
temperatureAxis->animateChangeLine(itemPos.temperature.expanded, animSpeed);
|
|
|
|
cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded, animSpeed);
|
2021-06-28 18:29:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
timeAxis->setPos(itemPos.time.on);
|
|
|
|
timeAxis->setLine(itemPos.time.expanded);
|
|
|
|
|
|
|
|
cylinderPressureAxis->setPos(itemPos.cylinder.on);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProfileScene::isPointOutOfBoundaries(const QPointF &point) const
|
|
|
|
{
|
|
|
|
double xpos = timeAxis->valueAt(point);
|
|
|
|
double ypos = profileYAxis->valueAt(point);
|
|
|
|
return xpos > timeAxis->maximum() ||
|
|
|
|
xpos < timeAxis->minimum() ||
|
|
|
|
ypos > profileYAxis->maximum() ||
|
|
|
|
ypos < profileYAxis->minimum();
|
|
|
|
}
|
|
|
|
|
|
|
|
ProfileItemPos::ProfileItemPos()
|
|
|
|
{
|
|
|
|
// Scene is *always* (double) 100 / 100.
|
|
|
|
// TODO: The relative scaling doesn't work well with large or small
|
|
|
|
// profiles and needs to be fixed.
|
|
|
|
|
2021-08-03 07:51:37 +00:00
|
|
|
dcLabel.on.setX(3);
|
|
|
|
dcLabel.on.setY(100);
|
|
|
|
dcLabel.off.setX(-10);
|
|
|
|
dcLabel.off.setY(100);
|
|
|
|
|
|
|
|
tankBar.on.setX(0);
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
tankBar.on.setY(91.95);
|
|
|
|
#else
|
|
|
|
tankBar.on.setY(86.4);
|
|
|
|
#endif
|
|
|
|
|
2021-06-28 18:29:46 +00:00
|
|
|
//Depth Axis Config
|
|
|
|
depth.on.setX(3);
|
|
|
|
depth.on.setY(3);
|
|
|
|
depth.off.setX(-2);
|
|
|
|
depth.off.setY(3);
|
|
|
|
depth.expanded.setP1(QPointF(0, 0));
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
depth.expanded.setP2(QPointF(0, 85));
|
|
|
|
#else
|
|
|
|
depth.expanded.setP2(QPointF(0, 65));
|
|
|
|
#endif
|
|
|
|
depth.shrinked.setP1(QPointF(0, 0));
|
|
|
|
depth.shrinked.setP2(QPointF(0, 55));
|
|
|
|
depth.intermediate.setP1(QPointF(0, 0));
|
|
|
|
depth.intermediate.setP2(QPointF(0, 65));
|
|
|
|
|
|
|
|
// Time Axis Config
|
|
|
|
time.on.setX(3);
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
time.on.setY(95);
|
|
|
|
#else
|
|
|
|
time.on.setY(89.5);
|
|
|
|
#endif
|
|
|
|
time.off.setX(3);
|
|
|
|
time.off.setY(110);
|
|
|
|
time.expanded.setP1(QPointF(0, 0));
|
|
|
|
time.expanded.setP2(QPointF(94, 0));
|
|
|
|
|
|
|
|
// Partial Gas Axis Config
|
|
|
|
partialPressure.on.setX(97);
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
partialPressure.on.setY(75);
|
|
|
|
#else
|
|
|
|
partialPressure.on.setY(70);
|
|
|
|
#endif
|
|
|
|
partialPressure.off.setX(110);
|
|
|
|
partialPressure.off.setY(63);
|
|
|
|
partialPressure.expanded.setP1(QPointF(0, 0));
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
partialPressure.expanded.setP2(QPointF(0, 19));
|
|
|
|
#else
|
|
|
|
partialPressure.expanded.setP2(QPointF(0, 20));
|
|
|
|
#endif
|
|
|
|
partialPressureWithTankBar = partialPressure;
|
|
|
|
partialPressureWithTankBar.expanded.setP2(QPointF(0, 17));
|
|
|
|
partialPressureTissue = partialPressure;
|
|
|
|
partialPressureTissue.on.setX(97);
|
|
|
|
partialPressureTissue.on.setY(65);
|
|
|
|
partialPressureTissue.expanded.setP2(QPointF(0, 16));
|
|
|
|
|
|
|
|
// cylinder axis config
|
|
|
|
cylinder.on.setX(3);
|
|
|
|
cylinder.on.setY(20);
|
|
|
|
cylinder.off.setX(-10);
|
|
|
|
cylinder.off.setY(20);
|
|
|
|
cylinder.expanded.setP1(QPointF(0, 15));
|
|
|
|
cylinder.expanded.setP2(QPointF(0, 50));
|
|
|
|
cylinder.shrinked.setP1(QPointF(0, 0));
|
|
|
|
cylinder.shrinked.setP2(QPointF(0, 20));
|
|
|
|
cylinder.intermediate.setP1(QPointF(0, 0));
|
|
|
|
cylinder.intermediate.setP2(QPointF(0, 20));
|
|
|
|
|
|
|
|
// Temperature axis config
|
|
|
|
temperature.on.setX(3);
|
|
|
|
temperature.off.setX(-10);
|
|
|
|
temperature.off.setY(40);
|
|
|
|
temperature.expanded.setP1(QPointF(0, 20));
|
|
|
|
temperature.expanded.setP2(QPointF(0, 33));
|
|
|
|
temperature.shrinked.setP1(QPointF(0, 2));
|
|
|
|
temperature.shrinked.setP2(QPointF(0, 12));
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
temperature.on.setY(60);
|
|
|
|
temperatureAll.on.setY(51);
|
|
|
|
temperature.intermediate.setP1(QPointF(0, 2));
|
|
|
|
temperature.intermediate.setP2(QPointF(0, 12));
|
|
|
|
#else
|
|
|
|
temperature.on.setY(51);
|
|
|
|
temperatureAll.on.setY(47);
|
|
|
|
temperature.intermediate.setP1(QPointF(0, 2));
|
|
|
|
temperature.intermediate.setP2(QPointF(0, 12));
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// Heart rate axis config
|
|
|
|
heartBeat.on.setX(3);
|
|
|
|
heartBeat.on.setY(82);
|
|
|
|
heartBeat.expanded.setP1(QPointF(0, 0));
|
|
|
|
heartBeat.expanded.setP2(QPointF(0, 10));
|
|
|
|
heartBeatWithTankBar = heartBeat;
|
|
|
|
heartBeatWithTankBar.expanded.setP2(QPointF(0, 7));
|
|
|
|
|
|
|
|
// Percentage axis config
|
|
|
|
percentage.on.setX(3);
|
|
|
|
percentage.on.setY(80);
|
|
|
|
percentage.expanded.setP1(QPointF(0, 0));
|
|
|
|
percentage.expanded.setP2(QPointF(0, 15));
|
|
|
|
percentageWithTankBar = percentage;
|
|
|
|
percentageWithTankBar.expanded.setP2(QPointF(0, 11.9));
|
|
|
|
}
|
2021-08-03 07:51:37 +00:00
|
|
|
|
|
|
|
void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsModel *plannerModel, bool inPlanner, bool instant, bool calcMax)
|
|
|
|
{
|
|
|
|
d = dIn;
|
|
|
|
dc = dcIn;
|
|
|
|
if (!d) {
|
|
|
|
clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!plannerModel) {
|
|
|
|
if (decoMode(false) == VPMB)
|
2021-08-13 07:56:46 +00:00
|
|
|
decoModelParameters->set(QString("VPM-B +%1").arg(prefs.vpmb_conservatism), getColor(PRESSURE_TEXT));
|
2021-08-03 07:51:37 +00:00
|
|
|
else
|
2021-08-13 07:56:46 +00:00
|
|
|
decoModelParameters->set(QString("GF %1/%2").arg(prefs.gflow).arg(prefs.gfhigh), getColor(PRESSURE_TEXT));
|
2021-08-03 07:51:37 +00:00
|
|
|
} else {
|
|
|
|
struct diveplan &diveplan = plannerModel->getDiveplan();
|
|
|
|
if (decoMode(inPlanner) == VPMB)
|
2021-08-13 07:56:46 +00:00
|
|
|
decoModelParameters->set(QString("VPM-B +%1").arg(diveplan.vpmb_conservatism), getColor(PRESSURE_TEXT));
|
2021-08-03 07:51:37 +00:00
|
|
|
else
|
2021-08-13 07:56:46 +00:00
|
|
|
decoModelParameters->set(QString("GF %1/%2").arg(diveplan.gflow).arg(diveplan.gfhigh), getColor(PRESSURE_TEXT));
|
2021-08-03 07:51:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const struct divecomputer *currentdc = get_dive_dc_const(d, dc);
|
|
|
|
if (!currentdc || !currentdc->samples)
|
|
|
|
return;
|
|
|
|
|
2021-08-03 08:32:08 +00:00
|
|
|
int animSpeed = instant || printMode ? 0 : qPrefDisplay::animation_speed();
|
2021-08-03 07:51:37 +00:00
|
|
|
|
|
|
|
bool setpointflag = (currentdc->divemode == CCR) && prefs.pp_graphs.po2;
|
|
|
|
bool sensorflag = setpointflag && prefs.show_ccr_sensors;
|
|
|
|
o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint);
|
|
|
|
ccrsensor1GasItem->setVisible(sensorflag);
|
|
|
|
ccrsensor2GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 1));
|
|
|
|
ccrsensor3GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 2));
|
|
|
|
ocpo2GasItem->setVisible((currentdc->divemode == PSCR) && prefs.show_scr_ocpo2);
|
|
|
|
|
|
|
|
updateVisibility();
|
|
|
|
|
|
|
|
// A non-null planner_ds signals to create_plot_info_new that the dive is currently planned.
|
|
|
|
struct deco_state *planner_ds = inPlanner && plannerModel ? &plannerModel->final_deco_state : nullptr;
|
|
|
|
|
|
|
|
/* This struct holds all the data that's about to be plotted.
|
|
|
|
* I'm not sure this is the best approach ( but since we are
|
|
|
|
* interpolating some points of the Dive, maybe it is... )
|
|
|
|
* The Calculation of the points should be done per graph,
|
|
|
|
* so I'll *not* calculate everything if something is not being
|
|
|
|
* shown.
|
|
|
|
* create_plot_info_new() automatically frees old plot data.
|
|
|
|
*/
|
|
|
|
create_plot_info_new(d, get_dive_dc_const(d, dc), &plotInfo, !calcMax, planner_ds);
|
|
|
|
|
|
|
|
int newMaxtime = get_maxtime(&plotInfo);
|
|
|
|
if (calcMax || newMaxtime > maxtime)
|
|
|
|
maxtime = newMaxtime;
|
|
|
|
|
|
|
|
/* Only update the max. depth if it's bigger than the current ones
|
|
|
|
* when we are dragging the handler to plan / add dive.
|
|
|
|
* otherwhise, update normally.
|
|
|
|
*/
|
|
|
|
int newMaxDepth = get_maxdepth(&plotInfo);
|
|
|
|
if (!calcMax) {
|
|
|
|
if (maxdepth < newMaxDepth) {
|
|
|
|
maxdepth = newMaxDepth;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
maxdepth = newMaxDepth;
|
|
|
|
}
|
|
|
|
|
|
|
|
dataModel->setDive(plotInfo);
|
|
|
|
|
|
|
|
// It seems that I'll have a lot of boilerplate setting the model / axis for
|
|
|
|
// each item, I'll mostly like to fix this in the future, but I'll keep at this for now.
|
|
|
|
profileYAxis->setMaximum(maxdepth);
|
2021-08-03 08:32:08 +00:00
|
|
|
profileYAxis->updateTicks(animSpeed);
|
2021-08-03 07:51:37 +00:00
|
|
|
|
|
|
|
temperatureAxis->setMinimum(plotInfo.mintemp);
|
|
|
|
temperatureAxis->setMaximum(plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000);
|
|
|
|
|
|
|
|
if (plotInfo.maxhr) {
|
|
|
|
int heartBeatAxisMin = lrint(plotInfo.minhr / 5.0 - 0.5) * 5;
|
|
|
|
int heartBeatAxisMax, heartBeatAxisTick;
|
|
|
|
if (plotInfo.maxhr - plotInfo.minhr < 40)
|
|
|
|
heartBeatAxisTick = 10;
|
|
|
|
else if (plotInfo.maxhr - plotInfo.minhr < 80)
|
|
|
|
heartBeatAxisTick = 20;
|
|
|
|
else if (plotInfo.maxhr - plotInfo.minhr < 100)
|
|
|
|
heartBeatAxisTick = 25;
|
|
|
|
else
|
|
|
|
heartBeatAxisTick = 50;
|
|
|
|
for (heartBeatAxisMax = heartBeatAxisMin; heartBeatAxisMax < plotInfo.maxhr; heartBeatAxisMax += heartBeatAxisTick);
|
|
|
|
heartBeatAxis->setMinimum(heartBeatAxisMin);
|
|
|
|
heartBeatAxis->setMaximum(heartBeatAxisMax + 1);
|
|
|
|
heartBeatAxis->setTickInterval(heartBeatAxisTick);
|
2021-08-28 18:15:57 +00:00
|
|
|
heartBeatAxis->updateTicks(animSpeed);
|
2021-08-03 07:51:37 +00:00
|
|
|
}
|
|
|
|
heartBeatAxis->setVisible(prefs.hrgraph && plotInfo.maxhr);
|
|
|
|
|
|
|
|
percentageAxis->setMinimum(0);
|
|
|
|
percentageAxis->setMaximum(100);
|
|
|
|
percentageAxis->setVisible(false);
|
2021-08-28 18:15:57 +00:00
|
|
|
percentageAxis->updateTicks(animSpeed);
|
2021-08-03 07:51:37 +00:00
|
|
|
|
|
|
|
if (calcMax)
|
|
|
|
timeAxis->setMaximum(maxtime);
|
|
|
|
|
|
|
|
int i, incr;
|
|
|
|
static int increments[8] = { 10, 20, 30, 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60 };
|
|
|
|
/* Time markers: at most every 10 seconds, but no more than 12 markers.
|
|
|
|
* We start out with 10 seconds and increment up to 30 minutes,
|
|
|
|
* depending on the dive time.
|
|
|
|
* This allows for 6h dives - enough (I hope) for even the craziest
|
|
|
|
* divers - but just in case, for those 8h depth-record-breaking dives,
|
|
|
|
* we double the interval if this still doesn't get us to 12 or fewer
|
|
|
|
* time markers */
|
|
|
|
i = 0;
|
|
|
|
while (i < 7 && maxtime / increments[i] > 12)
|
|
|
|
i++;
|
|
|
|
incr = increments[i];
|
|
|
|
while (maxtime / incr > 12)
|
|
|
|
incr *= 2;
|
|
|
|
timeAxis->setTickInterval(incr);
|
2021-08-03 08:32:08 +00:00
|
|
|
timeAxis->updateTicks(animSpeed);
|
2021-08-03 07:51:37 +00:00
|
|
|
cylinderPressureAxis->setMinimum(plotInfo.minpressure);
|
|
|
|
cylinderPressureAxis->setMaximum(plotInfo.maxpressure);
|
|
|
|
|
|
|
|
#ifdef SUBSURFACE_MOBILE
|
|
|
|
if (currentdc->divemode == CCR) {
|
|
|
|
gasYAxis->setPos(itemPos.partialPressure.on);
|
|
|
|
gasYAxis->setLine(itemPos.partialPressure.expanded);
|
|
|
|
|
|
|
|
tankItem->setVisible(false);
|
|
|
|
pn2GasItem->setVisible(false);
|
|
|
|
po2GasItem->setVisible(prefs.pp_graphs.po2);
|
|
|
|
pheGasItem->setVisible(false);
|
|
|
|
o2SetpointGasItem->setVisible(prefs.show_ccr_setpoint);
|
|
|
|
ccrsensor1GasItem->setVisible(prefs.show_ccr_sensors);
|
|
|
|
ccrsensor2GasItem->setVisible(prefs.show_ccr_sensors && (currentdc->no_o2sensors > 1));
|
|
|
|
ccrsensor3GasItem->setVisible(prefs.show_ccr_sensors && (currentdc->no_o2sensors > 1));
|
|
|
|
ocpo2GasItem->setVisible((currentdc->divemode == PSCR) && prefs.show_scr_ocpo2);
|
|
|
|
//when no gas graph, we can show temperature
|
|
|
|
if (!po2GasItem->isVisible() &&
|
|
|
|
!o2SetpointGasItem->isVisible() &&
|
|
|
|
!ccrsensor1GasItem->isVisible() &&
|
|
|
|
!ccrsensor2GasItem->isVisible() &&
|
|
|
|
!ccrsensor3GasItem->isVisible() &&
|
|
|
|
!ocpo2GasItem->isVisible())
|
|
|
|
temperatureItem->setVisible(true);
|
|
|
|
else
|
|
|
|
temperatureItem->setVisible(false);
|
|
|
|
} else {
|
|
|
|
tankItem->setVisible(prefs.tankbar);
|
|
|
|
gasYAxis->setPos(itemPos.partialPressure.off);
|
|
|
|
pn2GasItem->setVisible(false);
|
|
|
|
po2GasItem->setVisible(false);
|
|
|
|
pheGasItem->setVisible(false);
|
|
|
|
o2SetpointGasItem->setVisible(false);
|
|
|
|
ccrsensor1GasItem->setVisible(false);
|
|
|
|
ccrsensor2GasItem->setVisible(false);
|
|
|
|
ccrsensor3GasItem->setVisible(false);
|
|
|
|
ocpo2GasItem->setVisible(false);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
tankItem->setData(&plotInfo, d);
|
|
|
|
|
2021-08-03 08:32:08 +00:00
|
|
|
gasYAxis->update(animSpeed);
|
2021-08-03 07:51:37 +00:00
|
|
|
|
|
|
|
// Replot dive items
|
|
|
|
for (AbstractProfilePolygonItem *item: profileItems)
|
|
|
|
item->replot(d, inPlanner);
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
qDeleteAll(eventItems);
|
|
|
|
eventItems.clear();
|
|
|
|
struct event *event = currentdc->events;
|
|
|
|
struct gasmix lastgasmix = get_gasmix_at_time(d, get_dive_dc_const(d, dc), duration_t{1});
|
|
|
|
|
|
|
|
while (event) {
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
// if print mode is selected only draw headings, SP change, gas events or bookmark event
|
|
|
|
if (printMode) {
|
|
|
|
if (empty_string(event->name) ||
|
|
|
|
!(strcmp(event->name, "heading") == 0 ||
|
|
|
|
(same_string(event->name, "SP change") && event->time.seconds == 0) ||
|
|
|
|
event_is_gaschange(event) ||
|
|
|
|
event->type == SAMPLE_EVENT_BOOKMARK)) {
|
|
|
|
event = event->next;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
// printMode is always selected for SUBSURFACE_MOBILE due to font problems
|
|
|
|
// BUT events are wanted.
|
|
|
|
#endif
|
|
|
|
DiveEventItem *item = new DiveEventItem(d, event, lastgasmix, dataModel,
|
2021-08-09 14:48:08 +00:00
|
|
|
timeAxis, profileYAxis, animSpeed, dpr);
|
2021-08-03 07:51:37 +00:00
|
|
|
item->setZValue(2);
|
|
|
|
addItem(item);
|
|
|
|
eventItems.push_back(item);
|
|
|
|
if (event_is_gaschange(event))
|
|
|
|
lastgasmix = get_gasmix_from_event(d, event);
|
|
|
|
event = event->next;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only set visible the events that should be visible
|
|
|
|
Q_FOREACH (DiveEventItem *event, eventItems) {
|
|
|
|
event->setVisible(!event->shouldBeHidden());
|
|
|
|
}
|
|
|
|
QString dcText = get_dc_nickname(currentdc);
|
|
|
|
if (dcText == "planned dive")
|
|
|
|
dcText = tr("Planned dive");
|
|
|
|
else if (dcText == "manually added dive")
|
|
|
|
dcText = tr("Manually added dive");
|
|
|
|
else if (dcText.isEmpty())
|
|
|
|
dcText = tr("Unknown dive computer");
|
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
int nr;
|
|
|
|
if ((nr = number_of_computers(d)) > 1)
|
|
|
|
dcText += tr(" (#%1 of %2)").arg(dc + 1).arg(nr);
|
|
|
|
#endif
|
2021-08-13 07:56:46 +00:00
|
|
|
diveComputerText->set(dcText, getColor(TIME_TEXT, isGrayscale));
|
2021-08-03 07:51:37 +00:00
|
|
|
}
|
2021-08-04 05:27:41 +00:00
|
|
|
|
|
|
|
QImage ProfileScene::toImage(QSize size)
|
|
|
|
{
|
|
|
|
// The size of chart with respect to the scene is fixed - by convention - to 100.0.
|
|
|
|
// We add 2% to the height so that the dive computer name is not cut off.
|
|
|
|
QRectF sceneRect(0.0, 0.0, 100.0, 102.0);
|
|
|
|
|
|
|
|
QImage image(size, QImage::Format_ARGB32);
|
|
|
|
image.fill(getColor(::BACKGROUND, isGrayscale));
|
|
|
|
|
|
|
|
QPainter imgPainter(&image);
|
|
|
|
imgPainter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
imgPainter.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
|
|
render(&imgPainter, QRect(QPoint(), size), sceneRect, Qt::IgnoreAspectRatio);
|
|
|
|
imgPainter.end();
|
|
|
|
|
|
|
|
if (isGrayscale) {
|
|
|
|
// convert QImage to grayscale before rendering
|
|
|
|
for (int i = 0; i < image.height(); i++) {
|
|
|
|
QRgb *pixel = reinterpret_cast<QRgb *>(image.scanLine(i));
|
|
|
|
QRgb *end = pixel + image.width();
|
|
|
|
for (; pixel != end; pixel++) {
|
|
|
|
int gray_val = qGray(*pixel);
|
|
|
|
*pixel = QColor(gray_val, gray_val, gray_val).rgb();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProfileScene::draw(QPainter *painter, const QRect &pos)
|
|
|
|
{
|
|
|
|
QImage img = toImage(pos.size());
|
|
|
|
painter->drawImage(pos, img);
|
|
|
|
}
|