// SPDX-License-Identifier: GPL-2.0 #include "profile-widget/diveprofileitem.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 plot_info &pInfo, const DiveCartesianAxis &horizontal, const DiveCartesianAxis &vertical, DataAccessor accessor, double dpr) : hAxis(horizontal), vAxis(vertical), pInfo(pInfo), accessor(accessor), dpr(dpr), from(0), to(0) { setCacheMode(DeviceCoordinateCache); } AbstractProfilePolygonItem::~AbstractProfilePolygonItem() { } void AbstractProfilePolygonItem::clear() { setPolygon(QPolygonF()); texts.clear(); } static std::pair clip(double x1, double y1, double x2, double y2, double x) { double rel = fabs(x2 - x1) > 1e-10 ? (x - x1) / (x2 - x1) : 0.5; return { x, (y2 - y1) * rel + y1 }; } void AbstractProfilePolygonItem::clipStart(double &x, double &y, double next_x, double next_y) const { if (x < hAxis.minimum()) std::tie(x, y) = clip(x, y, next_x, next_y, hAxis.minimum()); } void AbstractProfilePolygonItem::clipStop(double &x, double &y, double prev_x, double prev_y) const { if (x > hAxis.maximum()) std::tie(x, y) = clip(prev_x, prev_y, x, y, hAxis.maximum()); } std::pair AbstractProfilePolygonItem::getPoint(int i) const { const auto &data = pInfo.entry; double x = data[i].sec; double y = accessor(data[i]); // Do clipping of first and last value if (i == from && i < to) { double next_x = data[i+1].sec; double next_y = accessor(data[i+1]); clipStart(x, y, next_x, next_y); } if (i == to - 1 && i > 0) { double prev_x = data[i-1].sec; double prev_y = accessor(data[i-1]); clipStop(x, y, prev_x, prev_y); } return { x, y }; } 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++) { auto [horizontalValue, verticalValue] = getPoint(i); 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); texts.clear(); } DiveProfileItem::DiveProfileItem(const plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, dpr) { } 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(); const auto &data = pInfo.entry; // This paints the colors of the velocities. for (int i = from + 1; i < to; i++) { QColor color = getColor((color_index_t)(VELOCITY_COLORS_START_IDX + data[i].velocity)); pen.setBrush(QBrush(color)); painter->setPen(pen); if (i - from < poly.count() - 1) painter->drawLine(poly[i - from], poly[i - from + 1]); } painter->restore(); } static bool comp_depth(const struct plot_data &p1, const struct plot_data &p2) { return p1.depth < p2.depth; } void DiveProfileItem::replot(const dive *d, int from, int to, bool in_planner) { makePolygon(from, to); if (polygon().isEmpty()) return; profileColor = pInfo.waypoint_above_ceiling ? QColor(Qt::red) : getColor(DEPTH_BOTTOM); /* Show any ceiling we may have encountered */ if (prefs.dcceiling && !prefs.redceiling) { QPolygonF p = polygon(); auto entry = pInfo.entry.begin() + (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)); // No point in searching peaks with less than three samples if (to - from < 3) return; const int half_interval = vAxis.getMinLabelDistance(hAxis); const int min_depth = 2000; // in mm const int min_prominence = 2000; // in mm (should this adapt to depth range?) const auto &data = pInfo.entry; const int max_peaks = (data[to - 1].sec - data[from].sec) / half_interval + 1; struct Peak { int range_from; int range_to; int peak; }; std::vector stack; stack.reserve(max_peaks); int highest_peak = std::max_element(data.begin() + from, data.begin() + to, comp_depth) - data.begin(); if (data[highest_peak].depth < min_depth) return; stack.push_back(Peak{ from, to, highest_peak }); while (!stack.empty()) { Peak act_peak = stack.back(); stack.pop_back(); plot_depth_sample(data[act_peak.peak], Qt::AlignHCenter | Qt::AlignTop, getColor(SAMPLE_DEEP)); // Skip half_interval seconds to the left and right of peak // and add new peaks if there is enough place. const plot_data &act_sample = data[act_peak.peak]; int valley = act_peak.peak; // Search for first sample outside minimum range to the right. int new_from; for (new_from = act_peak.peak + 1; new_from + 3 < act_peak.range_to; ++new_from) { if (data[new_from].sec > act_sample.sec + half_interval) break; if (data[new_from].depth < data[valley].depth) valley = new_from; } // Continue search until peaks reach the minimum prominence (height from valley). for ( ; new_from + 3 < act_peak.range_to; ++new_from) { if (data[new_from].depth >= data[valley].depth + min_prominence) { int new_peak = std::max_element(data.begin() + new_from, data.begin() + act_peak.range_to, comp_depth) - data.begin(); if (data[new_peak].depth < min_depth) break; stack.push_back(Peak{ new_from, act_peak.range_to, new_peak }); if (data[valley].depth >= min_depth) plot_depth_sample(data[valley], Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_SHALLOW)); break; } if (data[new_from].depth < data[valley].depth) valley = new_from; } valley = act_peak.peak; // Search for first sample outside minimum range to the left. int new_to; for (new_to = act_peak.peak - 1; new_to >= act_peak.range_from + 3; --new_to) { if (data[new_to].sec + half_interval < act_sample.sec) break; if (data[new_to].depth < data[valley].depth) valley = new_to; } // Continue search until peaks reach the minimum prominence (height from valley). for ( ; new_to >= act_peak.range_from + 3; --new_to) { if (data[new_to].depth >= data[valley].depth + min_prominence) { int new_peak = std::max_element(data.begin() + act_peak.range_from, data.begin() + new_to, comp_depth) - data.begin(); if (data[new_peak].depth < min_depth) break; stack.push_back(Peak{ act_peak.range_from, new_to, new_peak }); if (data[valley].depth >= min_depth) plot_depth_sample(data[valley], Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_SHALLOW)); break; } if (data[new_to].depth < data[valley].depth) valley = new_to; } } } void DiveProfileItem::plot_depth_sample(const struct plot_data &entry, QFlags flags, const QColor &color) { auto item = std::make_unique(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.push_back(std::move(item)); } DiveHeartrateItem::DiveHeartrateItem(const plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, 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; struct sec_hr { int sec; int hr; } hist[3] = {}; std::vector textItems; texts.clear(); // Ignore empty values. a heart rate of 0 would be a bad sign. QPolygonF poly; int interval = vAxis.getMinLabelDistance(hAxis); for (int i = from; i < to; i++) { auto [sec_double, hr_double] = getPoint(i); int hr = lrint(hr_double); if (!hr) continue; int sec = lrint(sec_double); QPointF point(hAxis.posAtValue(sec_double), vAxis.posAtValue(hr_double)); 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 a full label interval and less than a 20 beats change OR // if it's been less than half a label interval 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 + interval) && (abs(hr - last_printed_hr) < 20)) || (sec < last + interval / 2) || (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; auto text = std::make_unique(dpr, 0.7, flags, this); text->set(QStringLiteral("%1").arg(hr), getColor(HR_TEXT)); text->setPos(QPointF(hAxis.posAtValue(sec), vAxis.posAtValue(hr))); texts.push_back(std::move(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 plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, 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; double last = -300.0, last_printed_temp = 0.0, last_valid_temp = 0.0, sec = 0.0; std::vector> textItems; texts.clear(); // Ignore empty values. things do not look good with '0' as temperature in kelvin... QPolygonF poly; int interval = vAxis.getMinLabelDistance(hAxis); for (int i = from; i < to; i++) { auto [sec, mkelvin] = getPoint(i); if (mkelvin < 1.0) continue; QPointF point(hAxis.posAtValue(sec), vAxis.posAtValue(mkelvin)); poly.append(point); last_valid_temp = sec; /* don't print a temperature * if it's been less than a full label interval and less than a 2K change OR * if it's been less than a half label interval OR if the change from the * last print is less than .4K (and therefore less than 1F) */ if (((sec < last + interval) && (fabs(mkelvin - last_printed_temp) < 2000.0)) || (sec < last + interval / 2) || (fabs(mkelvin - last_printed_temp) < 400.0)) continue; last = sec; if (mkelvin > 200000.0) textItems.push_back({ static_cast(sec), static_cast(mkelvin) }); last_printed_temp = mkelvin; } setPolygon(poly); /* 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.0 && ((fabs(last_valid_temp - last_printed_temp) > 500.0) || (last < 0.75 * sec))) { textItems.push_back({ static_cast(sec), static_cast(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; auto text = std::make_unique(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.push_back(std::move(text)); } void DiveTemperatureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem*, QWidget*) { if (polygon().isEmpty()) return; painter->save(); painter->setPen(pen()); painter->drawPolyline(polygon()); painter->restore(); } static const double diveMeanDepthItemLabelScale = 0.8; DiveMeanDepthItem::DiveMeanDepthItem(const plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, dpr), labelWidth(DiveTextItem::getLabelSize(dpr, diveMeanDepthItemLabelScale, QStringLiteral("999.9ft")).first) { QPen pen; pen.setBrush(QBrush(getColor(::HR_AXIS))); pen.setCosmetic(true); pen.setWidth(2); setPen(pen); } // Apparently, there can be samples without mean depth? If not, remove these functions. std::pair DiveMeanDepthItem::getMeanDepth(int i) const { for ( ; i >= 0; --i) { const plot_data &entry = pInfo.entry[i]; if (entry.running_sum > 0) return { static_cast(entry.sec), static_cast(entry.running_sum) / entry.sec }; } return { 0.0, 0.0 }; } std::pair DiveMeanDepthItem::getNextMeanDepth(int first) const { int last = pInfo.nr; for (int i = first + 1; i < last; ++i) { const plot_data &entry = pInfo.entry[i]; if (entry.running_sum > 0) return { static_cast(entry.sec), static_cast(entry.running_sum) / entry.sec }; } return getMeanDepth(first); } void DiveMeanDepthItem::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; double prevSec = 0.0, prevMeanDepth = 0.0; QPolygonF poly; for (int i = from; i < to; i++) { auto [sec, meanDepth] = getMeanDepth(i); // Ignore empty values if (meanDepth == 0) continue; if (i == from && i < to) { auto [sec2, meanDepth2] = getNextMeanDepth(i); if (meanDepth2 > 0.0) clipStart(sec, meanDepth, sec2, meanDepth2); } if (i == to - 1 && i > 0) clipStop(sec, meanDepth, prevSec, prevMeanDepth); QPointF point(hAxis.posAtValue(sec), vAxis.posAtValue(meanDepth)); poly.append(point); prevSec = sec; prevMeanDepth = meanDepth; } setPolygon(poly); if (prevMeanDepth > 0.0) createTextItem(prevSec, prevMeanDepth); } 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(double lastSec, double lastMeanDepth) { texts.clear(); auto text = std::make_unique(dpr, diveMeanDepthItemLabelScale, Qt::AlignRight | Qt::AlignVCenter, this); text->set(get_depth_string(lrint(lastMeanDepth), true), getColor(TEMP_TEXT)); text->setPos(QPointF(hAxis.posAtValue(lastSec) + dpr, vAxis.posAtValue(lastMeanDepth))); texts.push_back(std::move(text)); } void DiveGasPressureItem::replot(const dive *d, int fromIn, int toIn, bool in_planner) { from = fromIn; to = toIn; std::vector plotted_cyl(pInfo.nr_cylinders, false); std::vector last_plotted(pInfo.nr_cylinders, 0.0); std::vector act_segments(pInfo.nr_cylinders); QPolygonF boundingPoly; segments.clear(); for (int i = from; i < to; i++) { auto entry = pInfo.entry.begin() + i; for (int cyl = 0; cyl < pInfo.nr_cylinders; cyl++) { double mbar = static_cast(get_plot_pressure(pInfo, i, cyl)); double time = static_cast(entry->sec); if (mbar < 1.0) continue; if (i == from && i < to - 1) { double mbar2 = static_cast(get_plot_pressure(pInfo, i+1, cyl)); double time2 = static_cast(entry[1].sec); if (mbar2 < 1.0) continue; clipStart(time, mbar, time2, mbar2); } if (i == to - 1 && i > from) { double mbar2 = static_cast(get_plot_pressure(pInfo, i-1, cyl)); double time2 = static_cast(entry[-1].sec); if (mbar2 < 1.0) continue; clipStop(time, mbar, time2, mbar2); } 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.0) color = MAGENTA; else color = getPressureColor(entry->density); } if (!act_segments[cyl].polygon.empty()) { /* Have we used this cylinder in the last two minutes? Continue */ if (time - act_segments[cyl].last.time <= 2*60) { act_segments[cyl].polygon.push_back({ point, color }); act_segments[cyl].last.time = time; act_segments[cyl].last.pressure = mbar; continue; } /* Finish the previous one, start a new one */ act_segments[cyl].cyl = cyl; segments.push_back(std::move(act_segments[cyl])); act_segments[cyl] = Segment(); } plotted_cyl[cyl] = true; act_segments[cyl].polygon.push_back({ point, color }); act_segments[cyl].last.time = time; act_segments[cyl].last.pressure = mbar; if (act_segments[cyl].first.pressure == 0.0) { act_segments[cyl].first.time = time; act_segments[cyl].first.pressure = mbar; } } } bool showDescriptions = false; for (int cyl = 0; cyl < pInfo.nr_cylinders; cyl++) { const cylinder_t *c = get_cylinder(d, cyl); if (!c) continue; showDescriptions = showDescriptions || (c && same_gasmix_cylinder(*c, cyl, d, true) != -1); if (act_segments[cyl].polygon.empty()) continue; act_segments[cyl].cyl = cyl; segments.push_back(std::move(act_segments[cyl])); } setPolygon(boundingPoly); texts.clear(); // These are offset values used to print the gas labels 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 startAlignVar = Qt::AlignTop; for (const Segment &segment: segments) { // Magic Y offset depending on whether we're aliging // the top of the text or the bottom of the text to // the pressure line. double y_offset = -0.5 * dpr; plotPressureValue(segment.first.pressure, segment.first.time, startAlignVar, y_offset); // For each cylinder, on right hand side of the curve, write cylinder pressure double x_offset = plotPressureValue(segment.last.pressure, segment.last.time, Qt::AlignTop | Qt::AlignLeft, y_offset) + 2; plotGasValue(segment.last.pressure, segment.last.time, get_cylinder(d, segment.cyl), Qt::AlignTop | Qt::AlignLeft, x_offset, y_offset, showDescriptions); /* Alternate alignment as we see cylinder use.. */ startAlignVar ^= Qt::AlignTop | Qt::AlignBottom; } } double DiveGasPressureItem::plotPressureValue(double mbar, double sec, QFlags align, double y_offset) { const char *unit; auto label = QStringLiteral("%1%2").arg(get_pressure_units(lrint(mbar), &unit)).arg(unit); auto text = std::make_unique(dpr, 1.0, align, this); text->set(label, getColor(PRESSURE_TEXT)); text->setPos(hAxis.posAtValue(sec), vAxis.posAtValue(mbar) + y_offset); texts.push_back(std::move(text)); return DiveTextItem::getLabelSize(dpr, 1.0, label).first; } void DiveGasPressureItem::plotGasValue(double mbar, double sec, const cylinder_t *cylinder, QFlags align, double x_offset, double y_offset, bool showDescription) { QString gas = get_gas_string(cylinder->gasmix); QString label; if (showDescription) label = QStringLiteral("(%1) %2").arg(QString::fromStdString(cylinder->type.description), gas); else label = gas; auto text = std::make_unique(dpr, 1.0, align, this); text->set(label, getColor(PRESSURE_TEXT)); text->setPos(hAxis.posAtValue(sec) - x_offset, vAxis.posAtValue(mbar) + y_offset); texts.push_back(std::move(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 Segment &segment: segments) { for (size_t i = 1; i < segment.polygon.size(); i++) { pen.setBrush(segment.polygon[i].col); painter->setPen(pen); painter->drawLine(segment.polygon[i - 1].pos, segment.polygon[i].pos); } } painter->restore(); } DiveCalculatedCeiling::DiveCalculatedCeiling(const plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, 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 plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : DiveCalculatedCeiling(pInfo, hAxis, vAxis, accessor, dpr) { } DiveReportedCeiling::DiveReportedCeiling(const plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, dpr) { } std::pair DiveReportedCeiling::getTimeValue(int i) const { const plot_data &entry = pInfo.entry[i]; int value = entry.in_deco && entry.stopdepth ? std::min(entry.stopdepth, entry.depth) : 0; return { static_cast(entry.sec), static_cast(value) }; } std::pair DiveReportedCeiling::getPoint(int i) const { auto [x,y] = getTimeValue(i); if (i == from && i < to) { auto [next_x, next_y] = getTimeValue(i + 1); clipStart(x, y, next_x, next_y); } if (i == to - 1 && i > 0) { auto [prev_x, prev_y] = getTimeValue(i - 1); clipStop(x, y, prev_x, prev_y); } return { x, y }; } void DiveReportedCeiling::replot(const dive *, int fromIn, int toIn, bool) { from = fromIn; to = toIn; QPolygonF p; for (int i = from; i < to; i++) { auto [sec, value] = getPoint(i); if (i == from) p.append(QPointF(hAxis.posAtValue(sec), vAxis.posAtValue(0.0))); p.append(QPointF(hAxis.posAtValue(sec), vAxis.posAtValue(value))); if (i == to - 1) p.append(QPointF(hAxis.posAtValue(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; 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++) { auto [time, value] = getPoint(i); 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); } 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)); for (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 plot_info &pInfo, const DiveCartesianAxis &hAxis, const DiveCartesianAxis &vAxis, DataAccessor accessor, double dpr) : AbstractProfilePolygonItem(pInfo, hAxis, vAxis, accessor, dpr), thresholdPtrMin(NULL), thresholdPtrMax(NULL) { } void PartialPressureGasItem::setColors(const QColor &normal, const QColor &alert) { normalColor = normal; alertColor = alert; }