subsurface/profile-widget/profilewidget2.cpp

1405 lines
48 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0
#include "profile-widget/profilewidget2.h"
#include "profile-widget/profilescene.h"
#include "core/device.h"
#include "core/event.h"
#include "core/eventtype.h"
#include "core/subsurface-string.h"
#include "core/qthelper.h"
#include "core/range.h"
#include "core/settings/qPrefTechnicalDetails.h"
#include "core/settings/qPrefPartialPressureGas.h"
#include "profile-widget/diveeventitem.h"
#include "profile-widget/divetextitem.h"
#include "profile-widget/divetooltipitem.h"
#include "profile-widget/divehandler.h"
#include "core/planner.h"
#include "profile-widget/ruleritem.h"
#include "core/pref.h"
#include "qt-models/diveplannermodel.h"
#include "qt-models/models.h"
#include "core/errorhelper.h"
#ifndef SUBSURFACE_MOBILE
#include "desktop-widgets/simplewidgets.h"
#include "commands/command.h"
#include "core/gettextfromc.h"
#include "core/imagedownloader.h"
#endif
#include <QMessageBox>
#include <QInputDialog>
#include <QWheelEvent>
#include <QMenu>
#include <QMimeData>
#include <QElapsedTimer>
#ifndef QT_NO_DEBUG
#include <QTableView>
#endif
// Constant describing at which z-level the thumbnails are located.
// We might add more constants here for easier customability.
static const double thumbnailBaseZValue = 100.0;
static double calcZoom(int zoomLevel)
{
// Base of exponential zoom function: one wheel-click will increase the zoom by 15%.
constexpr double zoomFactor = 1.15;
return zoomLevel == 0 ? 1.0 : pow(zoomFactor, zoomLevel);
}
ProfileWidget2::ProfileWidget2(DivePlannerPointsModel *plannerModelIn, double dpr, QWidget *parent) : QGraphicsView(parent),
profileScene(new ProfileScene(dpr, false, false)),
currentState(INIT),
plannerModel(plannerModelIn),
zoomLevel(0),
zoomedPosition(0.0),
#ifndef SUBSURFACE_MOBILE
toolTipItem(new ToolTipItem()),
#endif
d(nullptr),
dc(0),
empty(true),
panning(false),
#ifndef SUBSURFACE_MOBILE
mouseFollowerVertical(new DiveLineItem()),
mouseFollowerHorizontal(new DiveLineItem()),
rulerItem(new RulerItem2()),
#endif
shouldCalculateMax(true)
{
setupSceneAndFlags();
setupItemOnScene();
addItemsToScene();
scene()->installEventFilter(this);
#ifndef SUBSURFACE_MOBILE
setAcceptDrops(true);
connect(Thumbnailer::instance(), &Thumbnailer::thumbnailChanged, this, &ProfileWidget2::updateThumbnail, Qt::QueuedConnection);
connect(&diveListNotifier, &DiveListNotifier::picturesRemoved, this, &ProfileWidget2::picturesRemoved);
connect(&diveListNotifier, &DiveListNotifier::picturesAdded, this, &ProfileWidget2::picturesAdded);
connect(&diveListNotifier, &DiveListNotifier::cylinderEdited, this, &ProfileWidget2::profileChanged);
connect(&diveListNotifier, &DiveListNotifier::eventsChanged, this, &ProfileWidget2::profileChanged);
connect(&diveListNotifier, &DiveListNotifier::pictureOffsetChanged, this, &ProfileWidget2::pictureOffsetChanged);
connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &ProfileWidget2::divesChanged);
connect(&diveListNotifier, &DiveListNotifier::deviceEdited, this, &ProfileWidget2::replot);
connect(&diveListNotifier, &DiveListNotifier::diveComputerEdited, this, &ProfileWidget2::replot);
#endif // SUBSURFACE_MOBILE
#if !defined(QT_NO_DEBUG) && defined(SHOW_PLOT_INFO_TABLE)
QTableView *diveDepthTableView = new QTableView();
diveDepthTableView->setModel(profileScene->dataModel);
diveDepthTableView->show();
#endif
auto tec = qPrefTechnicalDetails::instance();
connect(tec, &qPrefTechnicalDetails::calcalltissuesChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::calcceilingChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::gflowChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::gfhighChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::dcceilingChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::eadChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::calcceiling3mChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::modChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::calcndlttsChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::hrgraphChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::rulergraphChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::show_sacChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::zoomed_plotChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::decoinfoChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::show_pictures_in_profileChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::tankbarChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::percentagegraphChanged , this, &ProfileWidget2::actionRequestedReplot);
connect(tec, &qPrefTechnicalDetails::infoboxChanged , this, &ProfileWidget2::actionRequestedReplot);
auto pp_gas = qPrefPartialPressureGas::instance();
connect(pp_gas, &qPrefPartialPressureGas::pheChanged, this, &ProfileWidget2::actionRequestedReplot);
connect(pp_gas, &qPrefPartialPressureGas::pn2Changed, this, &ProfileWidget2::actionRequestedReplot);
connect(pp_gas, &qPrefPartialPressureGas::po2Changed, this, &ProfileWidget2::actionRequestedReplot);
setProfileState();
}
ProfileWidget2::~ProfileWidget2()
{
}
#ifndef SUBSURFACE_MOBILE
void ProfileWidget2::keyPressEvent(QKeyEvent *e)
{
switch (e->key()) {
case Qt::Key_Delete: return keyDeleteAction();
case Qt::Key_Up: return keyUpAction();
case Qt::Key_Down: return keyDownAction();
case Qt::Key_Left: return keyLeftAction();
case Qt::Key_Right: return keyRightAction();
}
QGraphicsView::keyPressEvent(e);
}
#endif // SUBSURFACE_MOBILE
void ProfileWidget2::addItemsToScene()
{
#ifndef SUBSURFACE_MOBILE
scene()->addItem(toolTipItem);
scene()->addItem(rulerItem);
scene()->addItem(rulerItem->sourceNode());
scene()->addItem(rulerItem->destNode());
scene()->addItem(mouseFollowerHorizontal);
scene()->addItem(mouseFollowerVertical);
QPen pen(QColor(Qt::red).lighter());
pen.setWidth(0);
mouseFollowerHorizontal->setPen(pen);
mouseFollowerVertical->setPen(pen);
#endif
}
void ProfileWidget2::setupItemOnScene()
{
#ifndef SUBSURFACE_MOBILE
toolTipItem->setZValue(9998);
toolTipItem->setTimeAxis(profileScene->timeAxis);
rulerItem->setZValue(9997);
rulerItem->setAxis(profileScene->timeAxis, profileScene->profileYAxis);
mouseFollowerHorizontal->setZValue(9996);
mouseFollowerVertical->setZValue(9995);
#endif
}
void ProfileWidget2::replot()
{
plotDive(d, dc);
}
void ProfileWidget2::setupSceneAndFlags()
{
setScene(profileScene.get());
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setOptimizationFlags(QGraphicsView::DontSavePainterState);
setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform);
setMouseTracking(true);
}
void ProfileWidget2::resetZoom()
{
zoomLevel = 0;
zoomedPosition = 0.0;
}
// Currently just one dive, but the plan is to enable All of the selected dives.
void ProfileWidget2::plotDive(const struct dive *dIn, int dcIn, int flags)
{
d = dIn;
dc = dcIn;
if (!d) {
clear();
return;
}
// If there was no previously displayed dive, turn off animations
if (empty)
flags |= RenderFlags::Instant;
empty = false;
QElapsedTimer measureDuration; // let's measure how long this takes us (maybe we'll turn of TTL calculation later
measureDuration.start();
DivePlannerPointsModel *model = currentState == EDIT || currentState == PLAN ? plannerModel : nullptr;
bool inPlanner = currentState == PLAN;
double zoom = calcZoom(zoomLevel);
profileScene->plotDive(d, dc, model, inPlanner, flags & RenderFlags::Instant,
flags & RenderFlags::DontRecalculatePlotInfo,
shouldCalculateMax, zoom, zoomedPosition);
#ifndef SUBSURFACE_MOBILE
toolTipItem->setVisible(prefs.infobox);
toolTipItem->setPlotInfo(profileScene->plotInfo);
rulerItem->setVisible(prefs.rulergraph && currentState != PLAN && currentState != EDIT);
rulerItem->setPlotInfo(d, profileScene->plotInfo);
if ((currentState == EDIT || currentState == PLAN) && plannerModel) {
repositionDiveHandlers();
plannerModel->deleteTemporaryPlan();
}
// On zoom / pan don't recreate the picture thumbnails, only change their position.
if (flags & RenderFlags::DontRecalculatePlotInfo)
updateThumbnails();
else
plotPicturesInternal(d, flags & RenderFlags::Instant);
toolTipItem->refresh(d, mapToScene(mapFromGlobal(QCursor::pos())), currentState == PLAN);
#endif
// OK, how long did this take us? Anything above the second is way too long,
// so if we are calculation TTS / NDL then let's force that off.
qint64 elapsedTime = measureDuration.elapsed();
if (verbose)
report_info("Profile calculation for dive %d took %lld ms -- calculated ceiling preference is %d", d->number, elapsedTime, prefs.calcceiling);
if (elapsedTime > 1000 && prefs.calcndltts) {
qPrefTechnicalDetails::set_calcndltts(false);
report_error("%s", qPrintable(tr("Show NDL / TTS was disabled because of excessive processing time")));
}
}
void ProfileWidget2::divesChanged(const QVector<dive *> &dives, DiveField field)
{
// If the mode of the currently displayed dive changed, replot
if (field.mode &&
std::find(dives.begin(), dives.end(), d) != dives.end())
replot();
}
void ProfileWidget2::actionRequestedReplot(bool)
{
settingsChanged();
}
void ProfileWidget2::settingsChanged()
{
replot();
}
void ProfileWidget2::resizeEvent(QResizeEvent *event)
{
QGraphicsView::resizeEvent(event);
profileScene->resize(viewport()->size());
plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // disable animation on resize events
}
#ifndef SUBSURFACE_MOBILE
void ProfileWidget2::mousePressEvent(QMouseEvent *event)
{
QGraphicsView::mousePressEvent(event);
if (!event->isAccepted()) {
panning = true;
panningOriginalMousePosition = mapToScene(event->pos()).x();
panningOriginalProfilePosition = zoomedPosition;
viewport()->setCursor(Qt::ClosedHandCursor);
}
}
void ProfileWidget2::divePlannerHandlerClicked()
{
shouldCalculateMax = false;
}
void ProfileWidget2::divePlannerHandlerReleased()
{
if (currentState == EDIT)
emit stopMoved(1);
shouldCalculateMax = true;
replot();
}
void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event)
{
QGraphicsView::mouseReleaseEvent(event);
if (panning) {
panning = false;
viewport()->setCursor(Qt::ArrowCursor);
}
if (currentState == PLAN || currentState == EDIT) {
shouldCalculateMax = true;
replot();
}
}
#endif
void ProfileWidget2::setZoom(int level)
{
zoomLevel = level;
plotDive(d, dc, RenderFlags::DontRecalculatePlotInfo);
}
#ifndef SUBSURFACE_MOBILE
void ProfileWidget2::wheelEvent(QWheelEvent *event)
{
if (!d)
return;
if (event->angleDelta().x() && zoomLevel > 0) {
double oldPos = zoomedPosition;
zoomedPosition = profileScene->calcZoomPosition(calcZoom(zoomLevel),
oldPos,
oldPos - event->angleDelta().x());
if (oldPos != zoomedPosition)
plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo);
}
if (panning)
return; // No change in zoom level while panning.
if (event->buttons() == Qt::LeftButton)
return;
if (event->angleDelta().y() > 0 && zoomLevel < 20)
setZoom(++zoomLevel);
else if (event->angleDelta().y() < 0 && zoomLevel > 0)
setZoom(--zoomLevel);
}
void ProfileWidget2::mouseDoubleClickEvent(QMouseEvent *event)
{
if ((currentState == PLAN || currentState == EDIT) && plannerModel) {
QPointF mappedPos = mapToScene(event->pos());
if (!profileScene->pointOnProfile(mappedPos))
return;
int minutes = lrint(profileScene->timeAxis->valueAt(mappedPos) / 60);
int milimeters = lrint(profileScene->profileYAxis->valueAt(mappedPos) / M_OR_FT(1, 1)) * M_OR_FT(1, 1);
plannerModel->addStop(milimeters, minutes * 60);
if (currentState == EDIT)
emit stopAdded();
}
}
void ProfileWidget2::mouseMoveEvent(QMouseEvent *event)
{
QGraphicsView::mouseMoveEvent(event);
QPointF pos = mapToScene(event->pos());
if (panning) {
double oldPos = zoomedPosition;
zoomedPosition = profileScene->calcZoomPosition(calcZoom(zoomLevel),
panningOriginalProfilePosition,
panningOriginalMousePosition - pos.x());
if (oldPos != zoomedPosition)
plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // TODO: animations don't work when scrolling
}
toolTipItem->refresh(d, mapToScene(mapFromGlobal(QCursor::pos())), currentState == PLAN);
if (currentState == PLAN || currentState == EDIT) {
QRectF rect = profileScene->profileRegion;
auto [miny, maxy] = profileScene->profileYAxis->screenMinMax();
double x = std::clamp(pos.x(), rect.left(), rect.right());
double y = std::clamp(pos.y(), miny, maxy);
mouseFollowerHorizontal->setLine(rect.left(), y, rect.right(), y);
mouseFollowerVertical->setLine(x, rect.top(), x, rect.bottom());
}
}
bool ProfileWidget2::eventFilter(QObject *object, QEvent *event)
{
QGraphicsScene *s = qobject_cast<QGraphicsScene *>(object);
if (s && event->type() == QEvent::GraphicsSceneHelp) {
event->ignore();
return true;
}
return QGraphicsView::eventFilter(object, event);
}
#endif
template <typename T>
static void hideAll(const T &container)
{
for (auto &item: container)
item->setVisible(false);
}
void ProfileWidget2::clear()
{
currentState = INIT;
#ifndef SUBSURFACE_MOBILE
clearPictures();
#endif
disconnectPlannerModel();
profileScene->clear();
handles.clear();
gases.clear();
empty = true;
d = nullptr;
dc = 0;
}
void ProfileWidget2::setProfileState(const dive *dIn, int dcIn)
{
d = dIn;
dc = dcIn;
setProfileState();
}
void ProfileWidget2::setProfileState()
{
if (currentState == PROFILE)
return;
disconnectPlannerModel();
currentState = PROFILE;
setBackgroundBrush(getColor(::BACKGROUND, profileScene->isGrayscale));
#ifndef SUBSURFACE_MOBILE
toolTipItem->readPos();
toolTipItem->setVisible(prefs.infobox);
rulerItem->setVisible(prefs.rulergraph);
mouseFollowerHorizontal->setVisible(false);
mouseFollowerVertical->setVisible(false);
#endif
handles.clear();
gases.clear();
}
#ifndef SUBSURFACE_MOBILE
void ProfileWidget2::setEditState(const dive *d, int dc)
{
if (currentState == EDIT)
return;
setProfileState(d, dc);
mouseFollowerHorizontal->setVisible(true);
mouseFollowerVertical->setVisible(true);
connectPlannerModel();
currentState = EDIT;
pointsReset();
repositionDiveHandlers();
}
void ProfileWidget2::setPlanState(const dive *d, int dc)
{
if (currentState == PLAN)
return;
setProfileState(d, dc);
mouseFollowerHorizontal->setVisible(true);
mouseFollowerVertical->setVisible(true);
connectPlannerModel();
currentState = PLAN;
setBackgroundBrush(QColor("#D7E3EF"));
pointsReset();
repositionDiveHandlers();
}
#endif
bool ProfileWidget2::isPlanner() const
{
return currentState == PLAN;
}
#if 0 // TODO::: FINISH OR DISABLE
struct int ProfileWidget2::getEntryFromPos(QPointF pos)
{
// find the time stamp corresponding to the mouse position
int seconds = lrint(timeAxis->valueAt(pos));
for (int i = 0; i < plotInfo.nr; i++) {
if (plotInfo.entry[i].sec >= seconds)
return i;
}
return plotInfo.nr - 1;
}
#endif
#ifndef SUBSURFACE_MOBILE
static bool isDiveTextItem(const QGraphicsItem *item, const DiveTextItem *textItem)
{
while (item) {
if (item == textItem)
return true;
item = item->parentItem();
}
return false;
}
Planner: Improve Gas Handling in CCR Mode. This has become a bit of a catch-all overhaul of a large portion of the planner - I started out wanting to improve the CCR mode, but then as I started pulling all the other threads that needed addressing started to come with it. Improve how the gas selection is handled when planning dives in CCR mode, by making the type (OC / CCR) of segments dependent on the gas use type that was set for the selected gas. Add a preference to allow the user to chose to use OC gases as diluent, in a similar fashion to the original implementation. Hide gases that cannot be used in the currently selected dive mode in all drop downs. Include usage type in gas names if this is needed. Hide columns and disable elements in the 'Dive planner points' table if they can they can not be edited in the curently selected dive mode. Visually identify gases and usage types that are not appropriate for the currently selected dive mode. Move the 'Dive mode' selection to the top of the planner view, to accommodate the fact that this is a property of the dive and not a planner setting. Show a warning instead of the dive plan if the plan contains gases that are not usable in the selected dive mode. Fix the data entry for the setpoint in the 'Dive planner points' table. Fix problems with enabling / disabling planner settings when switching between dive modes. Refactor some names to make them more appropriate for their current usage. One point that is still open is to hide gas usage graphs in the planner profile if the gas isn't used for OC, as there is no way to meaningfully interpolate such usage. Signed-off-by: Michael Keller <github@ike.ch>
2024-05-15 05:23:39 +00:00
void ProfileWidget2::addGasChangeMenu(QMenu &m, QString menuTitle, const struct dive &d, int dcNr, int changeTime)
{
QMenu *gasChange = m.addMenu(menuTitle);
std::vector<std::pair<int, QString>> gases = get_dive_gas_list(&d, dcNr, true);
for (unsigned i = 0; i < gases.size(); i++) {
int cylinderIndex = gases[i].first;
gasChange->addAction(gases[i].second, [this, cylinderIndex, changeTime] { addGasSwitch(cylinderIndex, changeTime); });
}
}
void ProfileWidget2::contextMenuEvent(QContextMenuEvent *event)
{
if (currentState == EDIT || currentState == PLAN) {
QGraphicsView::contextMenuEvent(event);
return;
}
QMenu m;
if (!d)
return;
// figure out if we are ontop of the dive computer name in the profile
QGraphicsItem *sceneItem = itemAt(mapFromGlobal(event->globalPos()));
if (isDiveTextItem(sceneItem, profileScene->diveComputerText)) {
const struct divecomputer *currentdc = d->get_dc(dc);
if (!currentdc->deviceid && dc == 0 && d->number_of_computers() == 1)
// nothing to do, can't rename, delete or reorder
return;
// create menu to show when right clicking on dive computer name
if (dc > 0)
m.addAction(tr("Make first dive computer"), this, &ProfileWidget2::makeFirstDC);
if (d->number_of_computers() > 1) {
m.addAction(tr("Delete this dive computer"), this, &ProfileWidget2::deleteCurrentDC);
m.addAction(tr("Split this dive computer into own dive"), this, &ProfileWidget2::splitCurrentDC);
}
if (currentdc->deviceid)
m.addAction(tr("Rename this dive computer"), this, &ProfileWidget2::renameCurrentDC);
m.exec(event->globalPos());
// don't show the regular profile context menu
return;
}
// create the profile context menu
QPointF scenePos = mapToScene(mapFromGlobal(event->globalPos()));
qreal sec_val = profileScene->timeAxis->valueAt(scenePos);
int seconds = (sec_val < 0.0) ? 0 : (int)sec_val;
DiveEventItem *item = dynamic_cast<DiveEventItem *>(sceneItem);
// Add or edit Gas Change
if (d && item && item->ev.is_gaschange()) {
addGasChangeMenu(m, tr("Edit gas change"), *d, dc, item->ev.time.seconds);
} else if (d && d->cylinders.size() > 1) {
// if we have more than one gas, offer to switch to another one
const struct divecomputer *currentdc = d->get_dc(dc);
if (seconds == 0 || (!currentdc->samples.empty() && seconds <= currentdc->samples[0].time.seconds))
addGasChangeMenu(m, tr("Set initial gas"), *d, dc, 0);
else
addGasChangeMenu(m, tr("Add gas change"), *d, dc, seconds);
}
m.addAction(tr("Add setpoint change"), [this, seconds]() { ProfileWidget2::addSetpointChange(seconds); });
m.addAction(tr("Add bookmark"), [this, seconds]() { addBookmark(seconds); });
m.addAction(tr("Split dive into two"), [this, seconds]() { splitDive(seconds); });
divemode_loop loop(*d->get_dc(dc));
divemode_t divemode = loop.at(seconds);
QMenu *changeMode = m.addMenu(tr("Change divemode"));
if (divemode != OC)
changeMode->addAction(gettextFromC::tr(divemode_text_ui[OC]),
[this, seconds](){ addDivemodeSwitch(seconds, OC); });
if (divemode != CCR)
changeMode->addAction(gettextFromC::tr(divemode_text_ui[CCR]),
[this, seconds](){ addDivemodeSwitch(seconds, CCR); });
if (divemode != PSCR)
changeMode->addAction(gettextFromC::tr(divemode_text_ui[PSCR]),
[this, seconds](){ addDivemodeSwitch(seconds, PSCR); });
if (DiveEventItem *item = dynamic_cast<DiveEventItem *>(sceneItem)) {
m.addAction(tr("Remove event"), [this,item] { removeEvent(item); });
m.addAction(tr("Hide event"), [this, item] { hideEvent(item); });
m.addAction(tr("Hide events of type '%1'").arg(event_type_name(item->ev)),
[this, item] { hideEventType(item); });
if (item->ev.type == SAMPLE_EVENT_BOOKMARK)
m.addAction(tr("Edit name"), [this, item] { editName(item); });
#if 0 // TODO::: FINISH OR DISABLE
QPointF scenePos = mapToScene(event->pos());
int idx = getEntryFromPos(scenePos);
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
// this shows how to figure out if we should ask the user if they want adjust interpolated pressures
// at either side of a gas change
if (item->ev->type == SAMPLE_EVENT_GASCHANGE || item->ev->type == SAMPLE_EVENT_GASCHANGE2) {
int gasChangeIdx = idx;
while (gasChangeIdx > 0) {
--gasChangeIdx;
if (plotInfo.entry[gasChangeIdx].sec <= item->ev->time.seconds)
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
break;
}
const struct plot_data &gasChangeEntry = plotInfo.entry[newGasIdx];
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
// now gasChangeEntry points at the gas change, that entry has the final pressure of
// the old tank, the next entry has the starting pressure of the next tank
if (gasChangeIdx < plotInfo.nr - 1) {
int newGasIdx = gasChangeIdx + 1;
const struct plot_data &newGasEntry = plotInfo.entry[newGasIdx];
if (get_plot_sensor_pressure(&plotInfo, gasChangeIdx) == 0 || d->get_cylinder(gasChangeEntry->sensor[0])->sample_start.mbar == 0) {
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
// if we have no sensorpressure or if we have no pressure from samples we can assume that
// we only have interpolated pressure (the pressure in the entry may be stored in the sensor
// pressure field if this is the first or last entry for this tank... see details in gaspressures.c
pressure_t pressure;
pressure.mbar = get_plot_interpolated_pressure(&plotInfo, gasChangeIdx) ? : get_plot_sensor_pressure(&plotInfo, gasChangeIdx);
QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of cyl. %1 (currently interpolated as %2)")
Start cleaning up sensor indexing for multiple sensors This is a very timid start at making us actually use multiple sensors without the magical special case for just CCR oxygen tracking. It mainly does: - turn the "sample->sensor" index into an array of two indexes, to match the pressures themselves. - get rid of dive->{oxygen_cylinder_index,diluent_cylinder_index}, since a CCR dive should now simply set the sample->sensor[] indices correctly instead. - in a couple of places, start actually looping over the sensors rather than special-case the O2 case (although often the small "loops" are just unrolled, since it's just two cases. but in many cases we still end up only covering the zero sensor case, because the CCR O2 sensor code coverage was fairly limited. It's entirely possible (even likely) that this migth break some existing case: it tries to be a fairly direct ("stupid") translation of the old code, but unlike the preparatory patch this does actually does change some semantics. For example, right now the git loader code assumes that if the git save data contains a o2pressure entry, it just hardcodes the O2 sensor index to 1. In fact, one issue is going to simply be that our file formats do not have that multiple sensor format, but instead had very clearly encoded things as being the CCR O2 pressure sensor. But this is hopefully close to usable, and I will need feedback (and maybe test cases) from people who have existing CCR dives with pressure data. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org> Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2017-07-21 02:49:45 +00:00
.arg(gasChangeEntry->sensor[0] + 1).arg(get_pressure_string(pressure)));
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
}
if (get_plot_sensor_pressure(&plotInfo, newGasIdx) == 0 || d->get_cylinder(newGasEntry->sensor[0])->sample_start.mbar == 0) {
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
// we only have interpolated press -- see commend above
pressure_t pressure;
pressure.mbar = get_plot_interpolated_pressure(&plotInfo, newGasIdx) ? : get_plot_sensor_pressure(&plotInfo, newGasIdx);
QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of cyl. %1 (currently interpolated as %2)")
Start cleaning up sensor indexing for multiple sensors This is a very timid start at making us actually use multiple sensors without the magical special case for just CCR oxygen tracking. It mainly does: - turn the "sample->sensor" index into an array of two indexes, to match the pressures themselves. - get rid of dive->{oxygen_cylinder_index,diluent_cylinder_index}, since a CCR dive should now simply set the sample->sensor[] indices correctly instead. - in a couple of places, start actually looping over the sensors rather than special-case the O2 case (although often the small "loops" are just unrolled, since it's just two cases. but in many cases we still end up only covering the zero sensor case, because the CCR O2 sensor code coverage was fairly limited. It's entirely possible (even likely) that this migth break some existing case: it tries to be a fairly direct ("stupid") translation of the old code, but unlike the preparatory patch this does actually does change some semantics. For example, right now the git loader code assumes that if the git save data contains a o2pressure entry, it just hardcodes the O2 sensor index to 1. In fact, one issue is going to simply be that our file formats do not have that multiple sensor format, but instead had very clearly encoded things as being the CCR O2 pressure sensor. But this is hopefully close to usable, and I will need feedback (and maybe test cases) from people who have existing CCR dives with pressure data. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org> Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2017-07-21 02:49:45 +00:00
.arg(newGasEntry->sensor[0] + 1).arg(get_pressure_string(pressure)));
Profile: adjust tank pressures at gas change (incomplete) This code sets up the UI that will allow the user to adjust tank pressures at a gaschange event. The actual changing of the pressure is not implemented, yet, so this is disabled until someone finds time to do so. The scenario is this: a tec diver or sidemount diver without pressure sensors on at least one of their tanks still wants to reasonably accurately track gas consumption during a dive. The diver takes notes of the pressures at every tank switch (I find that odd, but apparently some cave divers indeed do that as they switch back and forth between different gases) and then wants to adjust the pressures in Subsurface to match those written down. One difficulty here is that the first and last pressure of a tank with no sensor data is still considered "sensor pressure" - this is basically an implementation detail in the code that is used to do the pressure interpolation to have constant-SAC pressure plots for tanks without sensors. So when we check if there is indeed no pressure data available at the gas change, we can't just work with the interpolated pressure - if this is the first (or last) time the tank was used, that pressure may be marked as sensor pressure. What's missing is the UI to enter the desired new pressure plus the black magic that actually inserts this into the dive in a way that doesn't break the assumptions in the rest of the code. I'm running out of time to do that but wanted to preserve this code so someone can continue this later. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2014-11-20 00:15:10 +00:00
}
}
}
#endif
}
if (any_event_types_hidden()) {
QMenu *m2 = m.addMenu(tr("Unhide event type"));
for (int i: hidden_event_types()) {
m2->addAction(event_type_name(i), [this, i]() {
show_event_type(i);
replot();
});
}
m2->addAction(tr("All event types"), this, &ProfileWidget2::unhideEventTypes);
}
const struct divecomputer *currentdc = d->get_dc(dc);
if (currentdc && std::any_of(currentdc->events.begin(), currentdc->events.end(),
[] (auto &ev) { return ev.hidden; }))
m.addAction(tr("Unhide individually hidden events of this dive"), this, &ProfileWidget2::unhideEvents);
m.exec(event->globalPos());
}
void ProfileWidget2::deleteCurrentDC()
{
if (d)
Command::deleteDiveComputer(mutable_dive(), dc);
}
void ProfileWidget2::splitCurrentDC()
{
if (d)
Command::splitDiveComputer(mutable_dive(), dc);
}
void ProfileWidget2::makeFirstDC()
{
if (d)
Command::moveDiveComputerToFront(mutable_dive(), dc);
}
void ProfileWidget2::renameCurrentDC()
{
if (!d)
return;
bool ok;
struct divecomputer *currentdc = mutable_dive()->get_dc(dc);
QString newName = QInputDialog::getText(this, tr("Edit nickname"),
tr("Set new nickname for %1 (serial %2):").arg(QString::fromStdString(currentdc->model)).
arg(QString::fromStdString(currentdc->serial)),
QLineEdit::Normal, QString::fromStdString(get_dc_nickname(currentdc)), &ok);
if (ok)
Command::editDeviceNickname(currentdc, newName);
}
void ProfileWidget2::hideEvent(DiveEventItem *item)
{
if (!d)
return;
struct divecomputer *currentdc = mutable_dive()->get_dc(dc);
int idx = item->idx;
if (!currentdc || idx < 0 || static_cast<size_t>(idx) >= currentdc->events.size())
return;
currentdc->events[idx].hidden = true;
item->hide();
}
void ProfileWidget2::hideEventType(DiveEventItem *item)
{
if (!item->ev.name.empty()) {
hide_event_type(&item->ev);
replot();
}
}
void ProfileWidget2::unhideEvents()
{
if (!d)
return;
struct divecomputer *currentdc = mutable_dive()->get_dc(dc);
if (!currentdc)
return;
for (auto &ev: currentdc->events)
ev.hidden = false;
for (DiveEventItem *item: profileScene->eventItems)
item->show();
}
void ProfileWidget2::unhideEventTypes()
{
show_all_event_types();
replot();
}
void ProfileWidget2::removeEvent(DiveEventItem *item)
{
const struct event &ev = item->ev;
if (QMessageBox::question(this, TITLE_OR_TEXT(
tr("Remove the selected event?"),
tr("%1 @ %2:%3").arg(QString::fromStdString(ev.name)).arg(ev.time.seconds / 60).arg(ev.time.seconds % 60, 2, 10, QChar('0'))),
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok)
Command::removeEvent(mutable_dive(), dc, item->idx);
}
void ProfileWidget2::addBookmark(int seconds)
{
if (d)
Command::addEventBookmark(mutable_dive(), dc, seconds);
}
void ProfileWidget2::addDivemodeSwitch(int seconds, int divemode)
{
if (d)
Command::addEventDivemodeSwitch(mutable_dive(), dc, seconds, divemode);
}
void ProfileWidget2::addSetpointChange(int seconds)
{
if (!d)
return;
SetpointDialog dialog(mutable_dive(), dc, seconds);
dialog.exec();
}
void ProfileWidget2::splitDive(int seconds)
{
if (!d)
return;
Command::splitDives(mutable_dive(), duration_t{ .seconds = seconds });
}
void ProfileWidget2::addGasSwitch(int tank, int seconds)
{
if (!d || tank < 0 || static_cast<size_t>(tank) >= d->cylinders.size())
return;
Command::addGasSwitch(mutable_dive(), dc, seconds, tank);
}
void ProfileWidget2::changeGas(int index, int newCylinderId)
{
if ((currentState == PLAN || currentState == EDIT) && plannerModel) {
QModelIndex modelIndex = plannerModel->index(index, DivePlannerPointsModel::GAS);
plannerModel->gasChange(modelIndex.sibling(modelIndex.row() + 1, modelIndex.column()), newCylinderId);
if (currentState == EDIT)
emit stopEdited();
}
}
void ProfileWidget2::editName(DiveEventItem *item)
{
if (!d)
return;
bool ok;
QString newName = QInputDialog::getText(this, tr("Edit name of bookmark"),
tr("Custom name:"), QLineEdit::Normal,
item->ev.name.c_str(), &ok);
if (ok && !newName.isEmpty()) {
if (newName.length() > 22) { //longer names will display as garbage.
QMessageBox lengthWarning;
lengthWarning.setText(tr("Name is too long!"));
lengthWarning.exec();
return;
}
Command::renameEvent(mutable_dive(), dc, item->idx, newName.toStdString());
}
}
void ProfileWidget2::connectPlannerModel()
{
connect(plannerModel, &DivePlannerPointsModel::dataChanged, this, &ProfileWidget2::replot);
connect(plannerModel, &DivePlannerPointsModel::cylinderModelEdited, this, &ProfileWidget2::replot);
connect(plannerModel, &DivePlannerPointsModel::modelReset, this, &ProfileWidget2::pointsReset);
connect(plannerModel, &DivePlannerPointsModel::rowsInserted, this, &ProfileWidget2::pointInserted);
connect(plannerModel, &DivePlannerPointsModel::rowsRemoved, this, &ProfileWidget2::pointsRemoved);
connect(plannerModel, &DivePlannerPointsModel::rowsMoved, this, &ProfileWidget2::pointsMoved);
}
#endif
void ProfileWidget2::disconnectPlannerModel()
{
#ifndef SUBSURFACE_MOBILE
if (plannerModel) {
disconnect(plannerModel, &DivePlannerPointsModel::dataChanged, this, &ProfileWidget2::replot);
disconnect(plannerModel, &DivePlannerPointsModel::cylinderModelEdited, this, &ProfileWidget2::replot);
disconnect(plannerModel, &DivePlannerPointsModel::modelReset, this, &ProfileWidget2::pointsReset);
disconnect(plannerModel, &DivePlannerPointsModel::rowsInserted, this, &ProfileWidget2::pointInserted);
disconnect(plannerModel, &DivePlannerPointsModel::rowsRemoved, this, &ProfileWidget2::pointsRemoved);
disconnect(plannerModel, &DivePlannerPointsModel::rowsMoved, this, &ProfileWidget2::pointsMoved);
}
#endif
}
int ProfileWidget2::handleIndex(const DiveHandler *h) const
{
auto it = std::find_if(handles.begin(), handles.end(),
[h] (const std::unique_ptr<DiveHandler> &h2)
{ return h == h2.get(); });
return it != handles.end() ? it - handles.begin() : -1;
}
#ifndef SUBSURFACE_MOBILE
DiveHandler *ProfileWidget2::createHandle()
{
Planner: Improve Gas Handling in CCR Mode. This has become a bit of a catch-all overhaul of a large portion of the planner - I started out wanting to improve the CCR mode, but then as I started pulling all the other threads that needed addressing started to come with it. Improve how the gas selection is handled when planning dives in CCR mode, by making the type (OC / CCR) of segments dependent on the gas use type that was set for the selected gas. Add a preference to allow the user to chose to use OC gases as diluent, in a similar fashion to the original implementation. Hide gases that cannot be used in the currently selected dive mode in all drop downs. Include usage type in gas names if this is needed. Hide columns and disable elements in the 'Dive planner points' table if they can they can not be edited in the curently selected dive mode. Visually identify gases and usage types that are not appropriate for the currently selected dive mode. Move the 'Dive mode' selection to the top of the planner view, to accommodate the fact that this is a property of the dive and not a planner setting. Show a warning instead of the dive plan if the plan contains gases that are not usable in the selected dive mode. Fix the data entry for the setpoint in the 'Dive planner points' table. Fix problems with enabling / disabling planner settings when switching between dive modes. Refactor some names to make them more appropriate for their current usage. One point that is still open is to hide gas usage graphs in the planner profile if the gas isn't used for OC, as there is no way to meaningfully interpolate such usage. Signed-off-by: Michael Keller <github@ike.ch>
2024-05-15 05:23:39 +00:00
DiveHandler *item = new DiveHandler(d, dc);
scene()->addItem(item);
connect(item, &DiveHandler::moved, this, &ProfileWidget2::divePlannerHandlerMoved);
connect(item, &DiveHandler::clicked, this, &ProfileWidget2::divePlannerHandlerClicked);
connect(item, &DiveHandler::released, this, &ProfileWidget2::divePlannerHandlerReleased);
return item;
}
QGraphicsSimpleTextItem *ProfileWidget2::createGas()
{
QGraphicsSimpleTextItem *gasChooseBtn = new QGraphicsSimpleTextItem();
scene()->addItem(gasChooseBtn);
gasChooseBtn->setZValue(10);
gasChooseBtn->setFlag(QGraphicsItem::ItemIgnoresTransformations);
return gasChooseBtn;
}
void ProfileWidget2::pointsReset()
{
handles.clear();
gases.clear();
int count = plannerModel->rowCount();
for (int i = 0; i < count; ++i) {
handles.emplace_back(createHandle());
gases.emplace_back(createGas());
}
}
void ProfileWidget2::pointInserted(const QModelIndex &, int from, int to)
{
for (int i = from; i <= to; ++i) {
handles.emplace(handles.begin() + i, createHandle());
gases.emplace(gases.begin() + i, createGas());
}
// Note: we don't replot the dive here, because when removing multiple
// points, these might trickle in one-by-one. Instead, the model will
// emit a data-changed signal.
}
void ProfileWidget2::pointsRemoved(const QModelIndex &, int start, int end)
{
// Qt's model/view API is mad. The end-point is inclusive, which means that the empty range is [0,-1]!
handles.erase(handles.begin() + start, handles.begin() + end + 1);
gases.erase(gases.begin() + start, gases.begin() + end + 1);
scene()->clearSelection();
// Note: we don't replot the dive here, because when removing multiple
// points, these might trickle in one-by-one. Instead, the model will
// emit a data-changed signal.
}
void ProfileWidget2::pointsMoved(const QModelIndex &, int start, int end, const QModelIndex &, int row)
{
move_in_range(handles, start, end + 1, row);
move_in_range(gases, start, end + 1, row);
}
void ProfileWidget2::repositionDiveHandlers()
{
hideAll(gases);
// Re-position the user generated dive handlers
for (int i = 0; i < plannerModel->rowCount(); i++) {
struct divedatapoint datapoint = plannerModel->at(i);
if (datapoint.time == 0) // those are the magic entries for tanks
continue;
DiveHandler *h = handles[i].get();
h->setVisible(datapoint.entered);
h->setPos(profileScene->timeAxis->posAtValue(datapoint.time), profileScene->profileYAxis->posAtValue(datapoint.depth.mm));
QPointF p1;
if (i == 0) {
if (prefs.drop_stone_mode)
// place the text on the straight line from the drop to stone position
p1 = QPointF(profileScene->timeAxis->posAtValue(datapoint.depth.mm / prefs.descrate),
profileScene->profileYAxis->posAtValue(datapoint.depth.mm));
else
// place the text on the straight line from the origin to the first position
p1 = QPointF(profileScene->timeAxis->posAtValue(0), profileScene->profileYAxis->posAtValue(0));
} else {
// place the text on the line from the last position
p1 = handles[i - 1]->pos();
}
QPointF p2 = handles[i]->pos();
QLineF line(p1, p2);
QPointF pos = line.pointAt(0.5);
gases[i]->setPos(pos);
if (datapoint.cylinderid >= 0 && datapoint.cylinderid < static_cast<int>(d->cylinders.size()))
gases[i]->setText(get_gas_string(d->get_cylinder(datapoint.cylinderid)->gasmix));
else
gases[i]->setText(QString());
gases[i]->setVisible(datapoint.entered &&
(i == 0 || gases[i]->text() != gases[i-1]->text()));
}
}
void ProfileWidget2::divePlannerHandlerMoved()
{
DiveHandler *activeHandler = qobject_cast<DiveHandler *>(sender());
int index = handleIndex(activeHandler);
// Grow the time axis if necessary.
int minutes = lrint(profileScene->timeAxis->valueAt(activeHandler->pos()) / 60);
if (minutes * 60 > profileScene->timeAxis->maximum() * 0.9)
profileScene->timeAxis->setBounds(0.0, profileScene->timeAxis->maximum() * 1.02);
divedatapoint data = plannerModel->at(index);
data.depth.mm = lrint(profileScene->profileYAxis->valueAt(activeHandler->pos()) / M_OR_FT(1, 1)) * M_OR_FT(1, 1);
data.time = lrint(profileScene->timeAxis->valueAt(activeHandler->pos()));
plannerModel->editStop(index, data);
}
std::vector<int> ProfileWidget2::selectedDiveHandleIndices() const
{
std::vector<int> res;
res.reserve(scene()->selectedItems().size());
for (QGraphicsItem *item: scene()->selectedItems()) {
if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(item))
res.push_back(handleIndex(handler));
}
return res;
}
void ProfileWidget2::keyDownAction()
{
if ((currentState != EDIT && currentState != PLAN) || !plannerModel)
return;
std::vector<int> handleIndices = selectedDiveHandleIndices();
for (int row: handleIndices) {
divedatapoint dp = plannerModel->at(row);
dp.depth.mm += M_OR_FT(1, 5);
plannerModel->editStop(row, dp);
}
if (currentState == EDIT && !handleIndices.empty())
emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves
}
void ProfileWidget2::keyUpAction()
{
if ((currentState != EDIT && currentState != PLAN) || !plannerModel)
return;
std::vector<int> handleIndices = selectedDiveHandleIndices();
for (int row: handleIndices) {
divedatapoint dp = plannerModel->at(row);
if (dp.depth.mm <= 0)
continue;
dp.depth.mm -= M_OR_FT(1, 5);
plannerModel->editStop(row, dp);
}
if (currentState == EDIT && !handleIndices.empty())
emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves
}
void ProfileWidget2::keyLeftAction()
{
if ((currentState != EDIT && currentState != PLAN) || !plannerModel)
return;
std::vector<int> handleIndices = selectedDiveHandleIndices();
for (int row: handleIndices) {
divedatapoint dp = plannerModel->at(row);
if (dp.time / 60 <= 0)
continue;
dp.time -= 60;
plannerModel->editStop(row, dp);
}
if (currentState == EDIT && !handleIndices.empty())
emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves
}
void ProfileWidget2::keyRightAction()
{
if ((currentState != EDIT && currentState != PLAN) || !plannerModel)
return;
std::vector<int> handleIndices = selectedDiveHandleIndices();
for (int row: handleIndices) {
divedatapoint dp = plannerModel->at(row);
dp.time += 60;
plannerModel->editStop(row, dp);
}
if (currentState == EDIT && !handleIndices.empty())
emit stopMoved(handleIndices.size()); // TODO: Accumulate key moves
}
void ProfileWidget2::keyDeleteAction()
{
if ((currentState != EDIT && currentState != PLAN) || !plannerModel)
return;
std::vector<int> handleIndices = selectedDiveHandleIndices();
// For now, we have to convert to QVector.
for (int index: handleIndices)
handles[index]->hide();
if (!handleIndices.empty()) {
plannerModel->removeSelectedPoints(handleIndices);
if (currentState == EDIT)
emit stopRemoved(handleIndices.size());
}
}
void ProfileWidget2::clearPictures()
{
pictures.clear();
}
static const double unscaledDurationLineWidth = 2.5;
static const double unscaledDurationLinePenWidth = 0.5;
// Reset the duration line after an image was moved or we found a new duration
void ProfileWidget2::updateDurationLine(PictureEntry &e)
{
if (e.duration.seconds > 0) {
// We know the duration of this video, reset the line symbolizing its extent accordingly
double begin = profileScene->timeAxis->posAtValue(e.offset.seconds);
double end = profileScene->timeAxis->posAtValue(e.offset.seconds + e.duration.seconds);
double y = e.thumbnail->y();
// Undo scaling for pen-width and line-width. For this purpose, we use the scaling of the y-axis.
double scale = transform().m22();
double durationLineWidth = unscaledDurationLineWidth / scale;
double durationLinePenWidth = unscaledDurationLinePenWidth / scale;
e.durationLine.reset(new QGraphicsRectItem(begin, y - durationLineWidth - durationLinePenWidth, end - begin, durationLineWidth));
e.durationLine->setPen(QPen(getColor(DURATION_LINE, profileScene->isGrayscale), durationLinePenWidth));
e.durationLine->setBrush(getColor(::BACKGROUND, profileScene->isGrayscale));
e.durationLine->setVisible(prefs.show_pictures_in_profile);
scene()->addItem(e.durationLine.get());
} else {
// This is either a picture or a video with unknown duration.
// In case there was a line (how could that be?) remove it.
e.durationLine.reset();
}
}
// This function is called asynchronously by the thumbnailer if a thumbnail
// was fetched from disk or freshly calculated.
void ProfileWidget2::updateThumbnail(QString filenameIn, QImage thumbnail, duration_t duration)
{
std::string filename = filenameIn.toStdString();
// Find the picture with the given filename
auto it = std::find_if(pictures.begin(), pictures.end(), [&filename](const PictureEntry &e)
{ return e.filename == filename; });
// If we didn't find a picture, it does either not belong to the current dive,
// or its timestamp is outside of the profile.
if (it != pictures.end()) {
// Replace the pixmap of the thumbnail with the newly calculated one.
int size = Thumbnailer::defaultThumbnailSize();
it->thumbnail->setPixmap(QPixmap::fromImage(thumbnail.scaled(size, size, Qt::KeepAspectRatio)));
// If the duration changed, update the line
if (duration.seconds != it->duration.seconds) {
it->duration = duration;
updateDurationLine(*it);
// If we created / removed a duration line, we have to update the thumbnail paint order.
updateThumbnailPaintOrder();
}
}
}
// Create a PictureEntry object and add its thumbnail to the scene if profile pictures are shown.
ProfileWidget2::PictureEntry::PictureEntry(offset_t offsetIn, const std::string &filenameIn, ProfileWidget2 *profile, bool synchronous) : offset(offsetIn),
filename(filenameIn),
thumbnail(new DivePictureItem)
{
QGraphicsScene *scene = profile->scene();
int size = Thumbnailer::defaultThumbnailSize();
scene->addItem(thumbnail.get());
thumbnail->setVisible(prefs.show_pictures_in_profile);
QImage img = Thumbnailer::instance()->fetchThumbnail(QString::fromStdString(filename), synchronous).scaled(size, size, Qt::KeepAspectRatio);
thumbnail->setPixmap(QPixmap::fromImage(img));
thumbnail->setFileUrl(QString::fromStdString(filename));
connect(thumbnail.get(), &DivePictureItem::removePicture, profile, &ProfileWidget2::removePicture);
}
// Define a default sort order for picture-entries: sort lexicographically by timestamp and filename.
bool ProfileWidget2::PictureEntry::operator< (const PictureEntry &e) const
{
// Use std::tie() for lexicographical sorting.
return std::tie(offset.seconds, filename) < std::tie(e.offset.seconds, e.filename);
}
// This function updates the paint order of the thumbnails and duration-lines, such that later
// thumbnails are painted on top of previous thumbnails and duration-lines on top of the thumbnail
// they belong to.
void ProfileWidget2::updateThumbnailPaintOrder()
{
if (!pictures.size())
return;
// To get the correct sort order, we place in thumbnails at equal z-distances
// between thumbnailBaseZValue and (thumbnailBaseZValue + 1.0).
// Duration-lines are placed between the thumbnails.
double z = thumbnailBaseZValue;
double step = 1.0 / (double)pictures.size();
for (PictureEntry &e: pictures) {
e.thumbnail->setBaseZValue(z);
if (e.durationLine)
e.durationLine->setZValue(z + step / 2.0);
z += step;
}
}
// Calculate the y-coordinates of the thumbnails, which are supposed to be sorted by x-coordinate.
// This will also change the order in which the thumbnails are painted, to avoid weird effects,
// when items are added later to the scene. This is done using the QGraphicsItem::packBefore() function.
// We can't use the z-value, because that will be modified on hoverEnter and hoverExit events.
void ProfileWidget2::calculatePictureYPositions()
{
double lastX = -1.0, lastY = 0.0;
const double yStart = 0.05; // At which depth the thumbnails start (in fraction of total depth).
const double yStep = 0.01; // Increase of depth for overlapping thumbnails (in fraction of total depth).
const double xSpace = 18.0 * profileScene->dpr; // Horizontal range in which thumbnails are supposed to be overlapping (in pixels).
const int maxDepth = 14; // Maximal depth of thumbnail stack (in thumbnails).
for (PictureEntry &e: pictures) {
// Invisible items are outside of the shown range - ignore.
if (!e.thumbnail->isVisible())
continue;
// Let's put the picture at the correct time, but at a fixed "depth" on the profile
// not sure this is ideal, but it seems to look right.
double x = e.thumbnail->x();
if (x < 0.0)
continue;
double y;
if (lastX >= 0.0 && fabs(x - lastX) < xSpace * profileScene->dpr && lastY <= (yStart + maxDepth * yStep) - 1e-10)
y = lastY + yStep;
else
y = yStart;
lastX = x;
lastY = y;
double yScreen = profileScene->timeAxis->screenPosition(y);
e.thumbnail->setY(yScreen);
updateDurationLine(e); // If we changed the y-position, we also have to change the duration-line.
}
updateThumbnailPaintOrder();
}
void ProfileWidget2::updateThumbnailXPos(PictureEntry &e)
{
// Here, we only set the x-coordinate of the picture. The y-coordinate
// will be set later in calculatePictureYPositions().
// Thumbnails outside of the shown range are hidden.
double time = e.offset.seconds;
if (time >= profileScene->timeAxis->minimum() && time <= profileScene->timeAxis->maximum()) {
double x = profileScene->timeAxis->posAtValue(time);
e.thumbnail->setX(x);
e.thumbnail->setVisible(true);
} else {
e.thumbnail->setVisible(false);
}
}
// This function resets the picture thumbnails of the current dive.
void ProfileWidget2::plotPictures()
{
plotPicturesInternal(d, false);
}
void ProfileWidget2::plotPicturesInternal(const struct dive *d, bool synchronous)
{
pictures.clear();
if (currentState == EDIT || currentState == PLAN)
return;
if (!d)
return;
// Fetch all pictures of the dive, but consider only those that are within the dive time.
// For each picture, create a PictureEntry object in the pictures-vector.
// emplace_back() constructs an object at the end of the vector. The parameters are passed directly to the constructor.
for (auto &picture: d->pictures) {
if (picture.offset.seconds > 0 && picture.offset.seconds <= d->duration.seconds)
pictures.emplace_back(picture.offset, picture.filename, this, synchronous);
}
if (pictures.empty())
return;
// Sort pictures by timestamp (and filename if equal timestamps).
// This will allow for proper location of the pictures on the profile plot.
std::sort(pictures.begin(), pictures.end());
updateThumbnails();
}
void ProfileWidget2::updateThumbnails()
{
// Calculate thumbnail positions. First the x-coordinates and and then the y-coordinates.
for (PictureEntry &e: pictures)
updateThumbnailXPos(e);
calculatePictureYPositions();
}
// Remove the pictures with the given filenames from the profile plot.
void ProfileWidget2::picturesRemoved(dive *d, QVector<QString> fileUrls)
{
// To remove the pictures, we use the std::remove_if() algorithm.
// std::remove_if() does not actually delete the elements, but moves
// them to the end of the given range. It returns an iterator to the
// end of the new range of non-deleted elements. A subsequent call to
// std::erase on the range of deleted elements then ultimately shrinks the vector.
// (c.f. erase-remove idiom: https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom)
auto it = std::remove_if(pictures.begin(), pictures.end(), [&fileUrls](const PictureEntry &e)
// Check whether filename of entry is in list of provided filenames
{ return std::find(fileUrls.begin(), fileUrls.end(), QString::fromStdString(e.filename)) != fileUrls.end(); });
pictures.erase(it, pictures.end());
calculatePictureYPositions();
}
void ProfileWidget2::picturesAdded(dive *d, QVector<picture> pics)
{
for (const picture &pic: pics) {
if (pic.offset.seconds > 0 && pic.offset.seconds <= d->duration.seconds) {
pictures.emplace_back(pic.offset, pic.filename, this, false);
updateThumbnailXPos(pictures.back());
}
}
// Sort pictures by timestamp (and filename if equal timestamps).
// This will allow for proper location of the pictures on the profile plot.
std::sort(pictures.begin(), pictures.end());
calculatePictureYPositions();
}
void ProfileWidget2::removePicture(const QString &fileUrl)
{
if (d)
Command::removePictures({ { mutable_dive(), { fileUrl.toStdString() } } });
}
void ProfileWidget2::profileChanged(dive *dive)
{
if (dive != d)
return; // Cylinders of a differnt dive than the shown one changed.
replot();
}
#endif
void ProfileWidget2::dropEvent(QDropEvent *event)
{
#ifndef SUBSURFACE_MOBILE
if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop") && d) {
QByteArray itemData = event->mimeData()->data("application/x-subsurfaceimagedrop");
QDataStream dataStream(&itemData, QIODevice::ReadOnly);
QString filename;
dataStream >> filename;
QPointF mappedPos = mapToScene(event->pos());
offset_t offset { .seconds = (int32_t)lrint(profileScene->timeAxis->valueAt(mappedPos)) };
Command::setPictureOffset(mutable_dive(), filename, offset);
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
#endif
}
#ifndef SUBSURFACE_MOBILE
void ProfileWidget2::pictureOffsetChanged(dive *dIn, QString filenameIn, offset_t offset)
{
if (dIn != d)
return; // Picture of a different dive than the one shown changed.
std::string filename = filenameIn.toStdString(); // TODO: can we move std::string through Qt's signal/slot system?
// Calculate time in dive where picture was dropped and whether the new position is during the dive.
bool duringDive = d && offset.seconds > 0 && offset.seconds < d->duration.seconds;
// A picture was drag&dropped onto the profile: We have four cases to consider:
// 1a) The image was already shown on the profile and is moved to a different position on the profile.
// Calculate the new position and move the picture.
// 1b) The image was on the profile and is moved outside of the dive time.
// Remove the picture.
// 2a) The image was not on the profile and is moved into the dive time.
// Add the picture to the profile.
// 2b) The image was not on the profile and is moved outside of the dive time.
// Do nothing.
auto oldPos = std::find_if(pictures.begin(), pictures.end(), [filename](const PictureEntry &e)
{ return e.filename == filename; });
if (oldPos != pictures.end()) {
// Cases 1a) and 1b): picture is on profile
if (duringDive) {
// Case 1a): move to new position
// First, find new position. Note that we also have to compare filenames,
// because it is quite easy to generate equal offsets.
auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e)
{ return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); });
// Set new offset
oldPos->offset = offset;
updateThumbnailXPos(*oldPos);
// Move image from old to new position
int oldIndex = oldPos - pictures.begin();
int newIndex = newPos - pictures.begin();
move_in_range(pictures, oldIndex, oldIndex + 1, newIndex);
} else {
// Case 1b): remove picture
pictures.erase(oldPos);
}
// In both cases the picture list changed, therefore we must recalculate the y-coordinatesA.
calculatePictureYPositions();
} else {
// Cases 2a) and 2b): picture not on profile. We only have to take action for
// the first case: picture is moved into dive-time.
if (duringDive) {
// Case 2a): add the picture at the appropriate position.
// The case move from outside-to-outside of the profile plot was handled by
// the "&& duringDive" condition in the if above.
// As for case 1a), we have to also consider filenames in the case of equal offsets.
auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e)
{ return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); });
// emplace() constructs the element at the given position in the vector.
// The parameters are passed directly to the contructor.
// The call returns an iterator to the new element (which might differ from
// the old iterator, since the buffer might have been reallocated).
newPos = pictures.emplace(newPos, offset, filename, this, false);
updateThumbnailXPos(*newPos);
calculatePictureYPositions();
}
}
}
#endif
void ProfileWidget2::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop")) {
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
void ProfileWidget2::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop")) {
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
struct dive *ProfileWidget2::mutable_dive() const
{
return const_cast<dive *>(d);
}