// SPDX-License-Identifier: GPL-2.0 #include "profile-widget/diveprofileitem.h" #include "qt-models/diveplotdatamodel.h" #include "profile-widget/divecartesianaxis.h" #include "profile-widget/divetextitem.h" #include "profile-widget/animationfunctions.h" #include "core/profile.h" #include "qt-models/diveplannermodel.h" #include "core/qthelper.h" #include "core/settings/qPrefTechnicalDetails.h" #include "core/settings/qPrefLog.h" #include "libdivecomputer/parser.h" #include "profile-widget/profilewidget2.h" AbstractProfilePolygonItem::AbstractProfilePolygonItem(const DivePlotDataModel &model, const DiveCartesianAxis &horizontal, int hColumn, const DiveCartesianAxis &vertical, int vColumn, double dpr) : hAxis(horizontal), vAxis(vertical), dataModel(model), hDataColumn(hColumn), vDataColumn(vColumn), dpr(dpr), from(0), to(0) { setCacheMode(DeviceCoordinateCache); } void AbstractProfilePolygonItem::clear() { setPolygon(QPolygonF()); qDeleteAll(texts); texts.clear(); } void AbstractProfilePolygonItem::makePolygon(int fromIn, int toIn) { from = fromIn; to = toIn; // Calculate the polygon. This is the polygon that will be painted on screen // on the ::paint method. Here we calculate the correct position of the points // regarting our cartesian plane ( made by the hAxis and vAxis ), the QPolygonF // is an array of QPointF's, so we basically get the point from the model, convert // to our coordinates, store. no painting is done here. QPolygonF poly; for (int i = from; i < to; i++) { double horizontalValue = dataModel.index(i, hDataColumn).data().toReal(); double verticalValue = dataModel.index(i, vDataColumn).data().toReal(); if (i == from) { QPointF point(hAxis.posAtValue(horizontalValue), vAxis.posAtValue(0.0)); poly.append(point); } QPointF point(hAxis.posAtValue(horizontalValue), vAxis.posAtValue(verticalValue)); poly.append(point); if (i == to - 1) { QPointF point(hAxis.posAtValue(horizontalValue), vAxis.posAtValue(0.0)); poly.append(point); } } setPolygon(poly); qDeleteAll(texts); texts.clear(); } DiveProfileItem::DiveProfileItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr), show_reported_ceiling(0), reported_ceiling_in_red(0) { } void DiveProfileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { if (polygon().isEmpty()) return; painter->save(); // This paints the Polygon + Background. I'm setting the pen to QPen() so we don't get a black line here, // after all we need to plot the correct velocities colors later. setPen(Qt::NoPen); QGraphicsPolygonItem::paint(painter, option, widget); // Here we actually paint the boundaries of the Polygon using the colors that the model provides. // Those are the speed colors of the dives. QPen pen; pen.setCosmetic(true); pen.setWidth(2); QPolygonF poly = polygon(); // This paints the colors of the velocities. for (int i = from + 1; i < to; i++) { QModelIndex colorIndex = dataModel.index(i, DivePlotDataModel::COLOR); pen.setBrush(QBrush(colorIndex.data(Qt::BackgroundRole).value())); painter->setPen(pen); if (i - from < poly.count() - 1) painter->drawLine(poly[i - from], poly[i - from + 1]); } painter->restore(); } void DiveProfileItem::replot(const dive *d, int from, int to, bool in_planner) { makePolygon(from, to); if (polygon().isEmpty()) return; show_reported_ceiling = prefs.dcceiling; reported_ceiling_in_red = prefs.redceiling; profileColor = dataModel.data().waypoint_above_ceiling ? QColor(Qt::red) : getColor(DEPTH_BOTTOM); /* Show any ceiling we may have encountered */ if (prefs.dcceiling && !prefs.redceiling) { QPolygonF p = polygon(); plot_data *entry = dataModel.data().entry + to - 1; for (int i = to - 1; i >= from; i--, entry--) { if (!entry->in_deco) { /* not in deco implies this is a safety stop, no ceiling */ p.append(QPointF(hAxis.posAtValue(entry->sec), vAxis.posAtValue(0))); } else { p.append(QPointF(hAxis.posAtValue(entry->sec), vAxis.posAtValue(qMin(entry->stopdepth, entry->depth)))); } } setPolygon(p); } // This is the blueish gradient that the Depth Profile should have. // It's a simple QLinearGradient with 2 stops, starting from top to bottom. QLinearGradient pat(0, polygon().boundingRect().top(), 0, polygon().boundingRect().bottom()); pat.setColorAt(1, profileColor); pat.setColorAt(0, getColor(DEPTH_TOP)); setBrush(QBrush(pat)); int last = -1; for (int i = from; i < to; i++) { struct plot_data *pd = dataModel.data().entry; struct plot_data *entry = pd + i; // "min/max" are the 9-minute window min/max indices struct plot_data *min_entry = pd + entry->min; struct plot_data *max_entry = pd + entry->max; if (entry->depth < 2000) continue; if ((entry == max_entry) && entry->depth / 100 != last) { plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_DEEP)); last = entry->depth / 100; } if ((entry == min_entry) && entry->depth / 100 != last) { plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignTop, getColor(SAMPLE_SHALLOW)); last = entry->depth / 100; } if (entry->depth != last) last = -1; } } void DiveProfileItem::plot_depth_sample(struct plot_data *entry, QFlags flags, const QColor &color) { DiveTextItem *item = new DiveTextItem(dpr, 1.0, flags, this); item->set(get_depth_string(entry->depth, true), color); item->setPos(hAxis.posAtValue(entry->sec), vAxis.posAtValue(entry->depth)); texts.append(item); } DiveHeartrateItem::DiveHeartrateItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr) { QPen pen; pen.setBrush(QBrush(getColor(::HR_PLOT))); pen.setCosmetic(true); pen.setWidth(1); setPen(pen); } void DiveHeartrateItem::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; int last = -300, last_printed_hr = 0, sec = 0; struct sec_hr { int sec; int hr; } hist[3] = {}; std::vector textItems; qDeleteAll(texts); texts.clear(); // Ignore empty values. a heart rate of 0 would be a bad sign. QPolygonF poly; for (int i = from; i < to; i++) { int hr = dataModel.index(i, vDataColumn).data().toInt(); if (!hr) continue; sec = dataModel.index(i, hDataColumn).data().toInt(); QPointF point(hAxis.posAtValue(sec), vAxis.posAtValue(hr)); poly.append(point); if (hr == hist[2].hr) // same as last one, no point in looking at printing continue; hist[0] = hist[1]; hist[1] = hist[2]; hist[2].sec = sec; hist[2].hr = hr; // don't print a HR // if it's not a local min / max // if it's been less than 5min and less than a 20 beats change OR // if it's been less than 2min OR if the change from the // last print is less than 10 beats // to test min / max requires three points, so we now look at the // previous one sec = hist[1].sec; hr = hist[1].hr; if ((hist[0].hr < hr && hr < hist[2].hr) || (hist[0].hr > hr && hr > hist[2].hr) || ((sec < last + 300) && (abs(hr - last_printed_hr) < 20)) || (sec < last + 120) || (abs(hr - last_printed_hr) < 10)) continue; last = sec; textItems.push_back({ sec, hr }); last_printed_hr = hr; } setPolygon(poly); for (size_t i = 0; i < textItems.size(); ++i) { auto [sec, hr] = textItems[i]; createTextItem(sec, hr, i == textItems.size() - 1); } } void DiveHeartrateItem::createTextItem(int sec, int hr, bool last) { int flags = last ? Qt::AlignLeft | Qt::AlignBottom : Qt::AlignRight | Qt::AlignBottom; DiveTextItem *text = new DiveTextItem(dpr, 0.7, flags, this); text->set(QString("%1").arg(hr), getColor(HR_TEXT)); text->setPos(QPointF(hAxis.posAtValue(sec), vAxis.posAtValue(hr))); texts.append(text); } void DiveHeartrateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) { if (polygon().isEmpty()) return; painter->save(); painter->setPen(pen()); painter->drawPolyline(polygon()); 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) { QPen pen; pen.setBrush(QBrush(getColor(::TEMP_PLOT))); pen.setCosmetic(true); pen.setWidth(2); setPen(pen); } void DiveTemperatureItem::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; int last = -300, last_printed_temp = 0, sec = 0, last_valid_temp = 0; std::vector> textItems; qDeleteAll(texts); texts.clear(); // Ignore empty values. things do not look good with '0' as temperature in kelvin... QPolygonF poly; for (int i = from; i < to; i++) { int mkelvin = dataModel.index(i, vDataColumn).data().toInt(); if (!mkelvin) continue; last_valid_temp = mkelvin; sec = dataModel.index(i, hDataColumn).data().toInt(); QPointF point(hAxis.posAtValue(sec), vAxis.posAtValue(mkelvin)); poly.append(point); /* don't print a temperature * if it's been less than 5min and less than a 2K change OR * if it's been less than 2min OR if the change from the * last print is less than .4K (and therefore less than 1F) */ if (((sec < last + 300) && (abs(mkelvin - last_printed_temp) < 2000)) || (sec < last + 120) || (abs(mkelvin - last_printed_temp) < 400)) continue; last = sec; if (mkelvin > 200000) textItems.push_back({ sec, mkelvin }); last_printed_temp = mkelvin; } setPolygon(poly); /* it would be nice to print the end temperature, if it's * different or if the last temperature print has been more * than a quarter of the dive back */ if (last_valid_temp > 200000 && ((abs(last_valid_temp - last_printed_temp) > 500) || ((double)last / (double)sec < 0.75))) { textItems.push_back({ sec, last_valid_temp }); } for (size_t i = 0; i < textItems.size(); ++i) { auto [sec, mkelvin] = textItems[i]; createTextItem(sec, mkelvin, i == textItems.size() - 1); } } void DiveTemperatureItem::createTextItem(int sec, int mkelvin, bool last) { temperature_t temp; temp.mkelvin = mkelvin; int flags = last ? Qt::AlignLeft | Qt::AlignBottom : Qt::AlignRight | Qt::AlignBottom; DiveTextItem *text = new DiveTextItem(dpr, 0.8, flags, this); text->set(get_temperature_string(temp, true), getColor(TEMP_TEXT)); text->setPos(QPointF(hAxis.posAtValue(sec), vAxis.posAtValue(mkelvin))); texts.append(text); } void DiveTemperatureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) { if (polygon().isEmpty()) return; painter->save(); painter->setPen(pen()); painter->drawPolyline(polygon()); painter->restore(); } DiveMeanDepthItem::DiveMeanDepthItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr) { QPen pen; pen.setBrush(QBrush(getColor(::HR_AXIS))); pen.setCosmetic(true); pen.setWidth(2); setPen(pen); lastRunningSum = 0.0; } void DiveMeanDepthItem::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; double meandepthvalue = 0.0; QPolygonF poly; plot_data *entry = dataModel.data().entry + from; for (int i = from; i < to; i++, entry++) { // Ignore empty values if (entry->running_sum == 0 || entry->sec == 0) continue; meandepthvalue = entry->running_sum / entry->sec; QPointF point(hAxis.posAtValue(entry->sec), vAxis.posAtValue(meandepthvalue)); poly.append(point); } lastRunningSum = meandepthvalue; setPolygon(poly); createTextItem(); } void DiveMeanDepthItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) { if (polygon().isEmpty()) return; painter->save(); painter->setPen(pen()); painter->drawPolyline(polygon()); painter->restore(); } void DiveMeanDepthItem::createTextItem() { plot_data *entry = dataModel.data().entry; int sec = to > 0 ? entry[to-1].sec : 0; qDeleteAll(texts); texts.clear(); DiveTextItem *text = new DiveTextItem(dpr, 0.8, Qt::AlignRight | Qt::AlignTop, this); text->set(get_depth_string(lrint(lastRunningSum), true), getColor(TEMP_TEXT)); text->setPos(QPointF(hAxis.posAtValue(sec) + 1, vAxis.posAtValue(lastRunningSum))); texts.append(text); } void DiveGasPressureItem::replot(const dive *d, int fromIn, int toIn, bool in_planner) { from = fromIn; to = toIn; const struct plot_info *pInfo = &dataModel.data(); std::vector plotted_cyl(pInfo->nr_cylinders, false); std::vector last_plotted(pInfo->nr_cylinders, 0); std::vector> poly(pInfo->nr_cylinders); QPolygonF boundingPoly; polygons.clear(); for (int i = from; i < to; i++) { const struct plot_data *entry = pInfo->entry + i; for (int cyl = 0; cyl < pInfo->nr_cylinders; cyl++) { int mbar = get_plot_pressure(pInfo, i, cyl); int time = entry->sec; if (!mbar) continue; QPointF point(hAxis.posAtValue(time), vAxis.posAtValue(mbar)); boundingPoly.push_back(point); QColor color; if (!in_planner) { if (entry->sac) color = getSacColor(entry->sac, d->sac); else color = MED_GRAY_HIGH_TRANS; } else { if (mbar < 0) color = MAGENTA; else color = getPressureColor(entry->density); } if (plotted_cyl[cyl]) { /* Have we used this cylinder in the last two minutes? Continue */ if (time - last_plotted[cyl] <= 2*60) { poly[cyl].push_back({ point, color }); last_plotted[cyl] = time; continue; } /* Finish the previous one, start a new one */ polygons.push_back(std::move(poly[cyl])); poly[cyl].clear(); } plotted_cyl[cyl] = true; last_plotted[cyl] = time; poly[cyl].push_back({ point, color }); } } for (int cyl = 0; cyl < pInfo->nr_cylinders; cyl++) { if (!plotted_cyl[cyl]) continue; polygons.push_back(poly[cyl]); } setPolygon(boundingPoly); qDeleteAll(texts); texts.clear(); std::vector seen_cyl(pInfo->nr_cylinders, false); std::vector last_pressure(pInfo->nr_cylinders, 0); std::vector last_time(pInfo->nr_cylinders, 0); // These are offset values used to print the gas lables and pressures on a // dive profile at appropriate Y-coordinates. We alternate aligning the // label and the gas pressure above and under the pressure line. // The values are historical, and we could try to pick the over/under // depending on whether this pressure is higher or lower than the average. // Right now it's just strictly alternating when you have multiple gas // pressures. QFlags alignVar = Qt::AlignTop; std::vector> align(pInfo->nr_cylinders); double axisRange = (vAxis.maximum() - vAxis.minimum())/1000; // Convert axis pressure range to bar double axisLog = log10(log10(axisRange)); for (int i = from; i < to; i++) { const struct plot_data *entry = pInfo->entry + i; for (int cyl = 0; cyl < pInfo->nr_cylinders; cyl++) { int mbar = get_plot_pressure(pInfo, i, cyl); if (!mbar) continue; if (!seen_cyl[cyl]) { double value_y_offset, label_y_offset; // Magic Y offset depending on whether we're aliging // the top of the text or the bottom of the text to // the pressure line. value_y_offset = -0.5; if (alignVar & Qt::AlignTop) { label_y_offset = 5 * axisLog; } else { label_y_offset = -7 * axisLog; } plotPressureValue(mbar, entry->sec, alignVar, value_y_offset); plotGasValue(mbar, entry->sec, get_cylinder(d, cyl)->gasmix, alignVar, label_y_offset); seen_cyl[cyl] = true; /* Alternate alignment as we see cylinder use.. */ align[cyl] = alignVar; alignVar ^= Qt::AlignTop | Qt::AlignBottom; } last_pressure[cyl] = mbar; last_time[cyl] = entry->sec; } } // For each cylinder, on right hand side of profile, write cylinder pressure for (int cyl = 0; cyl < pInfo->nr_cylinders; cyl++) { if (last_time[cyl]) { double value_y_offset = -0.5; plotPressureValue(last_pressure[cyl], last_time[cyl], align[cyl] | Qt::AlignLeft, value_y_offset); } } } void DiveGasPressureItem::plotPressureValue(int mbar, int sec, QFlags align, double pressure_offset) { const char *unit; int pressure = get_pressure_units(mbar, &unit); DiveTextItem *text = new DiveTextItem(dpr, 1.0, align, this); text->set(QString("%1%2").arg(pressure).arg(unit), getColor(PRESSURE_TEXT)); text->setPos(hAxis.posAtValue(sec), vAxis.posAtValue(mbar) + pressure_offset ); texts.push_back(text); } void DiveGasPressureItem::plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags align, double gasname_offset) { QString gas = get_gas_string(gasmix); DiveTextItem *text = new DiveTextItem(dpr, 1.0, align, this); text->set(gas, getColor(PRESSURE_TEXT)); text->setPos(hAxis.posAtValue(sec), vAxis.posAtValue(mbar) + gasname_offset ); texts.push_back(text); } void DiveGasPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) { if (polygon().isEmpty()) return; QPen pen; pen.setCosmetic(true); pen.setWidth(2); painter->save(); for (const std::vector &poly: polygons) { for (size_t i = 1; i < poly.size(); i++) { pen.setBrush(poly[i].col); painter->setPen(pen); painter->drawLine(poly[i - 1].pos, poly[i].pos); } } painter->restore(); } DiveCalculatedCeiling::DiveCalculatedCeiling(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr) { } void DiveCalculatedCeiling::replot(const dive *d, int from, int to, bool in_planner) { makePolygon(from, to); QLinearGradient pat(0, polygon().boundingRect().top(), 0, polygon().boundingRect().bottom()); pat.setColorAt(0, getColor(CALC_CEILING_SHALLOW)); pat.setColorAt(1, getColor(CALC_CEILING_DEEP)); setPen(QPen(QBrush(Qt::NoBrush), 0)); setBrush(pat); } void DiveCalculatedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { if (polygon().isEmpty()) return; QGraphicsPolygonItem::paint(painter, option, widget); } DiveCalculatedTissue::DiveCalculatedTissue(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : DiveCalculatedCeiling(model, hAxis, hColumn, vAxis, vColumn, dpr) { } DiveReportedCeiling::DiveReportedCeiling(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr) { } void DiveReportedCeiling::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; QPolygonF p; for (int i = from; i < to; i++) { const plot_data &entry = dataModel.data().entry[i]; if (i == from) p.append(QPointF(hAxis.posAtValue(entry.sec), vAxis.posAtValue(0))); if (entry.in_deco && entry.stopdepth) { p.append(QPointF(hAxis.posAtValue(entry.sec), vAxis.posAtValue(std::min(entry.stopdepth, entry.depth)))); } else { p.append(QPointF(hAxis.posAtValue(entry.sec), vAxis.posAtValue(0))); } } setPolygon(p); QLinearGradient pat(0, p.boundingRect().top(), 0, p.boundingRect().bottom()); // does the user want the ceiling in "surface color" or in red? if (prefs.redceiling) { pat.setColorAt(0, getColor(CEILING_SHALLOW)); pat.setColorAt(1, getColor(CEILING_DEEP)); } else { pat.setColorAt(0, getColor(BACKGROUND_TRANS)); pat.setColorAt(1, getColor(BACKGROUND_TRANS)); } setPen(QPen(QBrush(Qt::NoBrush), 0)); setBrush(pat); } void DiveReportedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { if (polygon().isEmpty()) return; QGraphicsPolygonItem::paint(painter, option, widget); } void PartialPressureGasItem::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; plot_data *entry = dataModel.data().entry + from; QPolygonF poly; QPolygonF alertpoly; alertPolygons.clear(); double threshold_min = 100.0; // yes, a ridiculous high partial pressure double threshold_max = 0.0; if (thresholdPtrMax) threshold_max = *thresholdPtrMax; if (thresholdPtrMin) threshold_min = *thresholdPtrMin; bool inAlertFragment = false; for (int i = from; i < to; i++, entry++) { double value = dataModel.index(i, vDataColumn).data().toDouble(); int time = dataModel.index(i, hDataColumn).data().toInt(); QPointF point(hAxis.posAtValue(time), vAxis.posAtValue(value)); poly.push_back(point); if (thresholdPtrMax && value >= threshold_max) { if (inAlertFragment) { alertPolygons.back().push_back(point); } else { alertpoly.clear(); alertpoly.push_back(point); alertPolygons.append(alertpoly); inAlertFragment = true; } } else if (thresholdPtrMin && value <= threshold_min) { if (inAlertFragment) { alertPolygons.back().push_back(point); } else { alertpoly.clear(); alertpoly.push_back(point); alertPolygons.append(alertpoly); inAlertFragment = true; } } else { inAlertFragment = false; } } setPolygon(poly); /* createPPLegend(trUtf8("pNâ‚‚"), getColor(PN2), legendPos); */ } void PartialPressureGasItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) { const qreal pWidth = 0.0; painter->save(); painter->setPen(QPen(normalColor, pWidth)); painter->drawPolyline(polygon()); QPolygonF poly; painter->setPen(QPen(alertColor, pWidth)); Q_FOREACH (const QPolygonF &poly, alertPolygons) painter->drawPolyline(poly); painter->restore(); } void PartialPressureGasItem::setThresholdSettingsKey(const double *prefPointerMin, const double *prefPointerMax) { thresholdPtrMin = prefPointerMin; thresholdPtrMax = prefPointerMax; } PartialPressureGasItem::PartialPressureGasItem(const DivePlotDataModel &model, const DiveCartesianAxis &hAxis, int hColumn, const DiveCartesianAxis &vAxis, int vColumn, double dpr) : AbstractProfilePolygonItem(model, hAxis, hColumn, vAxis, vColumn, dpr), thresholdPtrMin(NULL), thresholdPtrMax(NULL) { } void PartialPressureGasItem::setColors(const QColor &normal, const QColor &alert) { normalColor = normal; alertColor = alert; }